587 lines
25 KiB
TypeScript
587 lines
25 KiB
TypeScript
// frontend/src/components/SimpleTable.tsx
|
|
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
useReactTable, getCoreRowModel, getFilteredRowModel, getSortedRowModel,
|
|
getPaginationRowModel, flexRender, type CellContext,
|
|
type ColumnDef
|
|
} 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 skeletonStyles from './Skeleton.module.css'; // Importamos el estilo del esqueleto
|
|
|
|
import { equipoService, sectorService, usuarioService } from '../services/apiService';
|
|
import { PlusCircle, KeyRound, UserX, Pencil, ArrowUp, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
|
|
|
import ModalAnadirEquipo from './ModalAnadirEquipo';
|
|
import ModalEditarSector from './ModalEditarSector';
|
|
import ModalCambiarClave from './ModalCambiarClave';
|
|
import ModalDetallesEquipo from './ModalDetallesEquipo';
|
|
import ModalAnadirDisco from './ModalAnadirDisco';
|
|
import ModalAnadirRam from './ModalAnadirRam';
|
|
import ModalAnadirUsuario from './ModalAnadirUsuario';
|
|
import TableSkeleton from './TableSkeleton'; // Importamos el componente de esqueleto
|
|
|
|
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 [showScrollButton, setShowScrollButton] = useState(false);
|
|
const [selectedEquipo, setSelectedEquipo] = useState<Equipo | null>(null);
|
|
const [historial, setHistorial] = useState<any[]>([]);
|
|
const [isOnline, setIsOnline] = useState(false);
|
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
|
const [addingComponent, setAddingComponent] = useState<'disco' | 'ram' | 'usuario' | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
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 || isAddModalOpen) {
|
|
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, isAddModalOpen]);
|
|
|
|
useEffect(() => {
|
|
if (!selectedEquipo) return;
|
|
let isMounted = true;
|
|
const checkPing = async () => {
|
|
if (!selectedEquipo.ip) return;
|
|
try {
|
|
const data = await equipoService.ping(selectedEquipo.ip);
|
|
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]);
|
|
|
|
const handleCloseModal = () => {
|
|
if (addingComponent) {
|
|
toast.error("Debes cerrar la ventana de añadir componente primero.");
|
|
return;
|
|
}
|
|
setSelectedEquipo(null);
|
|
setIsOnline(false);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (selectedEquipo) {
|
|
equipoService.getHistory(selectedEquipo.hostname)
|
|
.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(() => {
|
|
setIsLoading(true);
|
|
Promise.all([
|
|
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' }));
|
|
setSectores(sectoresOrdenados);
|
|
}).catch(error => {
|
|
toast.error("No se pudieron cargar los datos iniciales.");
|
|
console.error("Error al cargar datos:", error);
|
|
}).finally(() => setIsLoading(false));
|
|
}, []);
|
|
|
|
const handleSectorChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
const value = e.target.value;
|
|
setSelectedSector(value);
|
|
if (value === 'Todos') setFilteredData(data);
|
|
else if (value === 'Asignar') setFilteredData(data.filter(i => !i.sector));
|
|
else setFilteredData(data.filter(i => i.sector?.nombre === value));
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!modalData) return;
|
|
const toastId = toast.loading('Guardando...');
|
|
try {
|
|
const sectorId = modalData.sector?.id ?? 0;
|
|
await equipoService.updateSector(modalData.id, sectorId);
|
|
const equipoActualizado = { ...modalData, sector_id: modalData.sector?.id };
|
|
const updateFunc = (prev: Equipo[]) => prev.map(e => e.id === modalData.id ? equipoActualizado : e);
|
|
setData(updateFunc);
|
|
setFilteredData(updateFunc);
|
|
if (selectedEquipo && selectedEquipo.id === modalData.id) {
|
|
setSelectedEquipo(equipoActualizado);
|
|
}
|
|
toast.success('Sector actualizado.', { id: toastId });
|
|
setModalData(null);
|
|
} catch (error) {
|
|
if (error instanceof Error) toast.error(error.message, { id: toastId });
|
|
}
|
|
};
|
|
|
|
const handleSavePassword = async (password: string) => {
|
|
if (!modalPasswordData) return;
|
|
const toastId = toast.loading('Actualizando...');
|
|
try {
|
|
await usuarioService.updatePassword(modalPasswordData.id, password);
|
|
|
|
const usernameToUpdate = modalPasswordData.username;
|
|
|
|
const newData = data.map(equipo => {
|
|
if (!equipo.usuarios.some(u => u.username === usernameToUpdate)) {
|
|
return equipo;
|
|
}
|
|
const updatedUsers = equipo.usuarios.map(user =>
|
|
user.username === usernameToUpdate ? { ...user, password: password } : user
|
|
);
|
|
return { ...equipo, usuarios: updatedUsers };
|
|
});
|
|
setData(newData);
|
|
if (selectedSector === 'Todos') setFilteredData(newData);
|
|
else if (selectedSector === 'Asignar') setFilteredData(newData.filter(i => !i.sector));
|
|
else setFilteredData(newData.filter(i => i.sector?.nombre === selectedSector));
|
|
if (selectedEquipo) {
|
|
const updatedSelectedEquipo = newData.find(e => e.id === selectedEquipo.id);
|
|
if (updatedSelectedEquipo) {
|
|
setSelectedEquipo(updatedSelectedEquipo);
|
|
}
|
|
}
|
|
|
|
toast.success(`Contraseña para '${usernameToUpdate}' actualizada en todos sus equipos.`, { id: toastId });
|
|
setModalPasswordData(null);
|
|
|
|
} catch (error) {
|
|
if (error instanceof Error) toast.error(error.message, { id: toastId });
|
|
}
|
|
};
|
|
|
|
|
|
const handleRemoveUser = async (hostname: string, username: string) => {
|
|
if (!window.confirm(`¿Quitar a ${username} de este equipo?`)) return;
|
|
const toastId = toast.loading(`Quitando a ${username}...`);
|
|
try {
|
|
await usuarioService.removeUserFromEquipo(hostname, username);
|
|
let equipoActualizado: Equipo | undefined;
|
|
const updateFunc = (prev: Equipo[]) => prev.map(e => {
|
|
if (e.hostname === hostname) {
|
|
equipoActualizado = { ...e, usuarios: e.usuarios.filter(u => u.username !== username) };
|
|
return equipoActualizado;
|
|
}
|
|
return e;
|
|
});
|
|
setData(updateFunc);
|
|
setFilteredData(updateFunc);
|
|
if (selectedEquipo && equipoActualizado && selectedEquipo.id === equipoActualizado.id) {
|
|
setSelectedEquipo(equipoActualizado);
|
|
}
|
|
toast.success(`${username} quitado.`, { id: toastId });
|
|
} catch (error) {
|
|
if (error instanceof Error) toast.error(error.message, { id: toastId });
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: number) => {
|
|
if (!window.confirm('¿Eliminar este equipo y sus relaciones?')) return false;
|
|
const toastId = toast.loading('Eliminando equipo...');
|
|
try {
|
|
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.message, { id: toastId });
|
|
return false;
|
|
}
|
|
};
|
|
|
|
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 {
|
|
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');
|
|
}
|
|
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);
|
|
} else if (type === 'ram' && typeof associationId === 'number') {
|
|
updatedEquipo.memoriasRam = equipo.memoriasRam.filter(m => m.equipoMemoriaRamId !== associationId);
|
|
} else if (type === 'usuario' && typeof associationId === 'object') {
|
|
updatedEquipo.usuarios = equipo.usuarios.filter(u => u.id !== associationId.usuarioId);
|
|
}
|
|
return updatedEquipo;
|
|
});
|
|
setData(updateState);
|
|
setFilteredData(updateState);
|
|
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 });
|
|
}
|
|
};
|
|
|
|
const handleCreateEquipo = async (nuevoEquipo: Omit<Equipo, 'id' | 'created_at' | 'updated_at'>) => {
|
|
const toastId = toast.loading('Creando nuevo equipo...');
|
|
try {
|
|
const equipoCreado = await equipoService.createManual(nuevoEquipo);
|
|
setData(prev => [...prev, equipoCreado]);
|
|
setFilteredData(prev => [...prev, equipoCreado]);
|
|
toast.success(`Equipo '${equipoCreado.hostname}' creado.`, { id: toastId });
|
|
setIsAddModalOpen(false);
|
|
} catch (error) {
|
|
if (error instanceof Error) toast.error(error.message, { id: toastId });
|
|
}
|
|
};
|
|
|
|
const handleEditEquipo = async (id: number, equipoEditado: any) => {
|
|
const toastId = toast.loading('Guardando cambios...');
|
|
try {
|
|
const equipoActualizadoDesdeBackend = await equipoService.updateManual(id, equipoEditado);
|
|
const updateState = (prev: Equipo[]) => prev.map(e => e.id === id ? equipoActualizadoDesdeBackend : e);
|
|
setData(updateState);
|
|
setFilteredData(updateState);
|
|
setSelectedEquipo(equipoActualizadoDesdeBackend);
|
|
toast.success('Equipo actualizado.', { id: toastId });
|
|
return true;
|
|
} catch (error) {
|
|
if (error instanceof Error) toast.error(error.message, { id: toastId });
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const handleAddComponent = async (type: 'disco' | 'ram' | 'usuario', componentData: any) => {
|
|
if (!selectedEquipo) return;
|
|
const toastId = toast.loading(`Añadiendo ${type}...`);
|
|
try {
|
|
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;
|
|
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);
|
|
} catch (error) {
|
|
if (error instanceof Error) toast.error(error.message, { id: toastId });
|
|
}
|
|
};
|
|
|
|
const columns: ColumnDef<Equipo>[] = [
|
|
{ header: "ID", accessorKey: "id", enableHiding: true },
|
|
{
|
|
header: "Nombre", accessorKey: "hostname",
|
|
cell: (info: CellContext<Equipo, any>) => (<button onClick={() => setSelectedEquipo(info.row.original)} className={styles.hostnameButton}>{info.row.original.hostname}</button>)
|
|
},
|
|
{ header: "IP", accessorKey: "ip", id: 'ip' },
|
|
{ header: "MAC", accessorKey: "mac", enableHiding: true },
|
|
{ header: "Motherboard", accessorKey: "motherboard" },
|
|
{ header: "CPU", accessorKey: "cpu" },
|
|
{ header: "RAM", accessorKey: "ram_installed", id: 'ram' },
|
|
{ header: "Discos", accessorFn: (row: Equipo) => row.discos?.map(d => `${d.mediatype} ${d.size}GB`).join(" | ") || "Sin discos" },
|
|
{ header: "OS", accessorKey: "os" },
|
|
{ header: "Arquitectura", accessorKey: "architecture", id: 'arch' },
|
|
{
|
|
header: "Usuarios y Claves",
|
|
id: 'usuarios',
|
|
cell: (info: CellContext<Equipo, any>) => {
|
|
const { row } = info;
|
|
const usuarios = row.original.usuarios || [];
|
|
return (
|
|
<div className={styles.userList}>
|
|
{usuarios.map((u: UsuarioEquipoDetalle) => (
|
|
<div key={u.id} className={styles.userItem}>
|
|
<span className={styles.userInfo}>
|
|
U: {u.username} - C: {u.password || 'N/A'}
|
|
</span>
|
|
|
|
<div className={styles.userActions}>
|
|
<button
|
|
onClick={() => setModalPasswordData(u)}
|
|
className={styles.tableButton}
|
|
data-tooltip-id={`edit-${u.id}`}
|
|
>
|
|
<KeyRound size={16} />
|
|
<Tooltip id={`edit-${u.id}`} className={styles.tooltip} place="top">Cambiar Clave</Tooltip>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleRemoveUser(row.original.hostname, u.username)}
|
|
className={styles.deleteUserButton}
|
|
data-tooltip-id={`remove-${u.id}`}
|
|
>
|
|
<UserX size={16} />
|
|
<Tooltip id={`remove-${u.id}`} className={styles.tooltip} place="top">Eliminar Usuario</Tooltip>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
header: "Sector", id: 'sector', accessorFn: (row: Equipo) => row.sector?.nombre || 'Asignar',
|
|
cell: (info: CellContext<Equipo, any>) => {
|
|
const { row } = info;
|
|
const sector = row.original.sector;
|
|
return (
|
|
<div className={styles.sectorContainer}>
|
|
<span className={`${styles.sectorName} ${sector ? styles.sectorNameAssigned : styles.sectorNameUnassigned}`}>{sector?.nombre || 'Asignar'}</span>
|
|
<button onClick={() => setModalData(row.original)} className={styles.tableButton} data-tooltip-id={`editSector-${row.original.id}`}><Pencil size={16} /><Tooltip id={`editSector-${row.original.id}`} className={styles.tooltip} place="top">Cambiar Sector</Tooltip></button>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
];
|
|
|
|
const table = useReactTable({
|
|
data: filteredData,
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
initialState: {
|
|
sorting: [
|
|
{ id: 'sector', desc: false },
|
|
{ id: 'hostname', desc: false }
|
|
],
|
|
columnVisibility: { id: false, mac: false },
|
|
pagination: {
|
|
pageSize: 15,
|
|
},
|
|
},
|
|
state: {
|
|
globalFilter,
|
|
},
|
|
onGlobalFilterChange: setGlobalFilter,
|
|
});
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<h2>Equipos (...)</h2>
|
|
<div className={`${skeletonStyles.skeleton}`} style={{ height: '40px', width: '160px' }}></div>
|
|
</div>
|
|
<div className={styles.controlsContainer}>
|
|
<div className={`${skeletonStyles.skeleton}`} style={{ height: '38px', width: '300px' }}></div>
|
|
<div className={`${skeletonStyles.skeleton}`} style={{ height: '38px', width: '200px' }}></div>
|
|
</div>
|
|
<div><p style={{ fontSize: '12px', fontWeight: 'bold' }}>** La tabla permite ordenar por múltiples columnas manteniendo shift al hacer click en la cabecera. **</p></div>
|
|
<div className={`${skeletonStyles.skeleton}`} style={{ height: '54px', marginBottom: '1rem' }}></div>
|
|
<TableSkeleton rows={15} columns={11} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const PaginacionControles = (
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '1rem 0' }}>
|
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
|
<button onClick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()} className={styles.tableButton}>
|
|
<ChevronsLeft size={18} />
|
|
</button>
|
|
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} className={styles.tableButton}>
|
|
<ChevronLeft size={18} />
|
|
</button>
|
|
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} className={styles.tableButton}>
|
|
<ChevronRight size={18} />
|
|
</button>
|
|
<button onClick={() => table.setPageIndex(table.getPageCount() - 1)} disabled={!table.getCanNextPage()} className={styles.tableButton}>
|
|
<ChevronsRight size={18} />
|
|
</button>
|
|
</div>
|
|
<span>
|
|
Página{' '}
|
|
<strong>
|
|
{table.getState().pagination.pageIndex + 1} de {table.getPageCount()}
|
|
</strong>
|
|
</span>
|
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
|
<span>| Ir a pág:</span>
|
|
<input
|
|
type="number"
|
|
defaultValue={table.getState().pagination.pageIndex + 1}
|
|
onChange={e => {
|
|
const page = e.target.value ? Number(e.target.value) - 1 : 0;
|
|
table.setPageIndex(page);
|
|
}}
|
|
style={{ width: '60px' }}
|
|
className={styles.searchInput}
|
|
/>
|
|
<select
|
|
value={table.getState().pagination.pageSize}
|
|
onChange={e => {
|
|
table.setPageSize(Number(e.target.value));
|
|
}}
|
|
className={styles.sectorSelect}
|
|
>
|
|
{[10, 15, 25, 50, 100].map(pageSize => (
|
|
<option key={pageSize} value={pageSize}>
|
|
Mostrar {pageSize}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<h2>Equipos ({table.getFilteredRowModel().rows.length})</h2>
|
|
<button
|
|
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
onClick={() => setIsAddModalOpen(true)}
|
|
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
|
>
|
|
<PlusCircle size={18} /> Añadir Equipo
|
|
</button>
|
|
</div>
|
|
<div className={styles.controlsContainer}>
|
|
<input type="text" placeholder="Buscar en todos los campos..." value={globalFilter} onChange={(e) => setGlobalFilter(e.target.value)} className={styles.searchInput} style={{ width: '300px' }} />
|
|
<b>Sector:</b>
|
|
<select value={selectedSector} onChange={handleSectorChange} className={styles.sectorSelect}>
|
|
<option value="Todos">-Todos-</option>
|
|
<option value="Asignar">-Asignar-</option>
|
|
{sectores.map(s => (<option key={s.id} value={s.nombre}>{s.nombre}</option>))}
|
|
</select>
|
|
</div>
|
|
<div><p style={{ fontSize: '12px', fontWeight: 'bold' }}>** La tabla permite ordenar por múltiples columnas manteniendo shift al hacer click en la cabecera. **</p></div>
|
|
|
|
{PaginacionControles}
|
|
<div style={{ overflowX: 'auto', maxHeight: '70vh', border: '1px solid #dee2e6', borderRadius: '8px' }}>
|
|
<table className={styles.table}>
|
|
<thead>
|
|
{table.getHeaderGroups().map(hg => (
|
|
<tr key={hg.id}>
|
|
{hg.headers.map(h => {
|
|
const classNames = [styles.th];
|
|
if (h.id === 'ip') classNames.push(styles.thIp);
|
|
if (h.id === 'ram') classNames.push(styles.thRam);
|
|
if (h.id === 'arch') classNames.push(styles.thArch);
|
|
if (h.id === 'usuarios') classNames.push(styles.thUsers);
|
|
if (h.id === 'sector') classNames.push(styles.thSector);
|
|
|
|
return (
|
|
<th key={h.id} className={classNames.join(' ')} onClick={h.column.getToggleSortingHandler()}>
|
|
{flexRender(h.column.columnDef.header, h.getContext())}
|
|
{h.column.getIsSorted() && (<span className={styles.sortIndicator}>{h.column.getIsSorted() === 'asc' ? '⇑' : '⇓'}</span>)}
|
|
</th>
|
|
);
|
|
})}
|
|
</tr>
|
|
))}
|
|
</thead>
|
|
<tbody>
|
|
{table.getRowModel().rows.map(row => (
|
|
<tr key={row.id} className={styles.tr}>
|
|
{row.getVisibleCells().map(cell => {
|
|
const classNames = [styles.td];
|
|
if (cell.column.id === 'ip') classNames.push(styles.tdIp);
|
|
if (cell.column.id === 'ram') classNames.push(styles.tdRam);
|
|
if (cell.column.id === 'arch') classNames.push(styles.tdArch);
|
|
if (cell.column.id === 'usuarios') classNames.push(styles.tdUsers);
|
|
if (cell.column.id === 'sector') classNames.push(styles.tdSector);
|
|
|
|
return (
|
|
<td key={cell.id} className={classNames.join(' ')}>
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{PaginacionControles}
|
|
|
|
{showScrollButton && (<button onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} className={styles.scrollToTop} title="Volver arriba"><ArrowUp size={24} /></button>)}
|
|
|
|
{modalData && <ModalEditarSector modalData={modalData} setModalData={setModalData} sectores={sectores} onClose={() => setModalData(null)} onSave={handleSave} />}
|
|
|
|
{modalPasswordData && <ModalCambiarClave usuario={modalPasswordData} onClose={() => setModalPasswordData(null)} onSave={handleSavePassword} />}
|
|
|
|
{selectedEquipo && (
|
|
<ModalDetallesEquipo
|
|
equipo={selectedEquipo}
|
|
isOnline={isOnline}
|
|
historial={historial}
|
|
onClose={handleCloseModal}
|
|
onDelete={handleDelete}
|
|
onRemoveAssociation={handleRemoveAssociation}
|
|
onEdit={handleEditEquipo}
|
|
sectores={sectores}
|
|
onAddComponent={type => setAddingComponent(type)}
|
|
isChildModalOpen={addingComponent !== null}
|
|
/>
|
|
)}
|
|
|
|
{isAddModalOpen && <ModalAnadirEquipo sectores={sectores} onClose={() => setIsAddModalOpen(false)} onSave={handleCreateEquipo} />}
|
|
|
|
{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>
|
|
);
|
|
};
|
|
|
|
export default SimpleTable; |