feat: Mejora reactividad de la UI y funcionalidad de tablas

- **Corrección de Sincronización de Estado:** Se soluciona un problema crítico donde las modificaciones de datos (ej. cambiar una contraseña o eliminar una asociación) no se reflejaban visualmente en la tabla hasta que se recargaba la página. Se ha refactorizado el manejo del estado para garantizar que todos los componentes se actualicen instantáneamente después de una acción.

- **Actualización Global de Contraseñas:** Se mejora la lógica de actualización de contraseñas. Ahora, al cambiar la clave de un usuario, el cambio se refleja en **todos** los equipos a los que dicho usuario está asociado en la tabla, no solo en la fila desde donde se inició la acción.

- **Mejoras en Gestión de Componentes:**
  - Se implementa la librería `@tanstack/react-table` en el componente `GestionComponentes.tsx`.
  - La tabla de "Administración" ahora cuenta con filtrado global de registros y ordenamiento por columnas, mejorando su usabilidad y manteniendo consistencia con la tabla principal de equipos.

- **Corrección de Tipos en TypeScript:** Se resuelven errores de tipo (`Property 'id' does not exist on type 'never'`) en la definición de las columnas de la tabla (`SimpleTable.tsx`) mediante el tipado explícito con `CellContext`, mejorando la robustez y la experiencia de desarrollo.
This commit is contained in:
2025-10-09 11:28:39 -03:00
parent bb3144a71b
commit 3893f917fc
2 changed files with 230 additions and 136 deletions

View File

@@ -2,7 +2,8 @@
import React, { useEffect, useState } from 'react';
import {
useReactTable, getCoreRowModel, getFilteredRowModel, getSortedRowModel,
getPaginationRowModel, flexRender, type CellContext
getPaginationRowModel, flexRender, type CellContext,
type ColumnDef
} from '@tanstack/react-table';
import { Tooltip } from 'react-tooltip';
import toast from 'react-hot-toast';
@@ -125,13 +126,18 @@ const SimpleTable = () => {
};
const handleSave = async () => {
if (!modalData || !modalData.sector) return;
if (!modalData) return;
const toastId = toast.loading('Guardando...');
try {
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);
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) {
@@ -143,28 +149,57 @@ const SimpleTable = () => {
if (!modalPasswordData) return;
const toastId = toast.loading('Actualizando...');
try {
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)
}));
setData(updatedData);
setFilteredData(updatedData);
toast.success(`Contraseña actualizada.`, { id: toastId });
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);
const updateFunc = (prev: Equipo[]) => prev.map(e => e.hostname === hostname ? { ...e, usuarios: e.usuarios.filter(u => u.username !== username) } : e);
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 });
@@ -188,7 +223,6 @@ const SimpleTable = () => {
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 = '';
@@ -204,7 +238,6 @@ const SimpleTable = () => {
} 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 };
@@ -217,11 +250,9 @@ const SimpleTable = () => {
}
return updatedEquipo;
});
setData(updateState);
setFilteredData(updateState);
setSelectedEquipo(prev => prev ? updateState([prev])[0] : null);
if (selectedEquipo) {
await refreshHistory(selectedEquipo.hostname);
}
@@ -248,13 +279,10 @@ const SimpleTable = () => {
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);
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) {
@@ -275,30 +303,26 @@ const SimpleTable = () => {
default: throw new Error('Tipo de componente no válido');
}
await serviceCall;
// 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);
} catch (error) {
if (error instanceof Error) toast.error(error.message, { id: toastId });
}
};
const columns = [
// --- DEFINICIÓN DE COLUMNAS CORREGIDA ---
const columns: ColumnDef<Equipo>[] = [
{ header: "ID", accessorKey: "id", enableHiding: true },
{
header: "Nombre", accessorKey: "hostname",
cell: ({ row }: CellContext<Equipo, any>) => (<button onClick={() => setSelectedEquipo(row.original)} className={styles.hostnameButton}>{row.original.hostname}</button>)
cell: (info: CellContext<Equipo, any>) => (<button onClick={() => setSelectedEquipo(info.row.original)} className={styles.hostnameButton}>{info.row.original.hostname}</button>)
},
{ header: "IP", accessorKey: "ip" },
{ header: "MAC", accessorKey: "mac", enableHiding: true },
@@ -310,7 +334,8 @@ const SimpleTable = () => {
{ header: "Arquitectura", accessorKey: "architecture" },
{
header: "Usuarios y Claves",
cell: ({ row }: CellContext<Equipo, any>) => {
cell: (info: CellContext<Equipo, any>) => {
const { row } = info;
const usuarios = row.original.usuarios || [];
return (
<div className={styles.userList}>
@@ -347,12 +372,13 @@ const SimpleTable = () => {
},
{
header: "Sector", id: 'sector', accessorFn: (row: Equipo) => row.sector?.nombre || 'Asignar',
cell: ({ row }: CellContext<Equipo, any>) => {
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.id}`}><Tooltip id={`editSector-${row.id}`} className={styles.tooltip} place="top">Cambiar Sector</Tooltip></button>
<button onClick={() => setModalData(row.original)} className={styles.tableButton} data-tooltip-id={`editSector-${row.original.id}`}><Tooltip id={`editSector-${row.original.id}`} className={styles.tooltip} place="top">Cambiar Sector</Tooltip></button>
</div>
);
}