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

@@ -1,6 +1,14 @@
// frontend/src/components/GestionComponentes.tsx // frontend/src/components/GestionComponentes.tsx
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo, useCallback } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import {
useReactTable,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
flexRender,
type SortingState,
} from '@tanstack/react-table';
import styles from './SimpleTable.module.css'; import styles from './SimpleTable.module.css';
import { adminService } from '../services/apiService'; import { adminService } from '../services/apiService';
@@ -17,14 +25,20 @@ interface RamValue {
conteo: number; conteo: number;
} }
type ComponentValue = TextValue | RamValue;
const GestionComponentes = () => { const GestionComponentes = () => {
const [componentType, setComponentType] = useState('os'); const [componentType, setComponentType] = useState('os');
const [valores, setValores] = useState<(TextValue | RamValue)[]>([]); const [valores, setValores] = useState<ComponentValue[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [valorAntiguo, setValorAntiguo] = useState(''); const [valorAntiguo, setValorAntiguo] = useState('');
const [valorNuevo, setValorNuevo] = useState(''); const [valorNuevo, setValorNuevo] = useState('');
// Estados para la tabla (filtrado y ordenamiento)
const [globalFilter, setGlobalFilter] = useState('');
const [sorting, setSorting] = useState<SortingState>([]);
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
adminService.getComponentValues(componentType) adminService.getComponentValues(componentType)
@@ -37,11 +51,11 @@ const GestionComponentes = () => {
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
}, [componentType]); }, [componentType]);
const handleOpenModal = (valor: string) => { const handleOpenModal = useCallback((valor: string) => {
setValorAntiguo(valor); setValorAntiguo(valor);
setValorNuevo(valor); setValorNuevo(valor);
setIsModalOpen(true); setIsModalOpen(true);
}; }, []);
const handleUnificar = async () => { const handleUnificar = async () => {
const toastId = toast.loading('Unificando valores...'); const toastId = toast.loading('Unificando valores...');
@@ -56,36 +70,34 @@ const GestionComponentes = () => {
} }
}; };
// 2. FUNCIÓN DELETE ACTUALIZADA: Ahora maneja un grupo const handleDeleteRam = useCallback(async (ramGroup: RamValue) => {
const handleDeleteRam = async (ramGroup: RamValue) => {
if (!window.confirm("¿Estás seguro de eliminar todas las entradas maestras para este tipo de RAM? Esta acción es irreversible.")) { if (!window.confirm("¿Estás seguro de eliminar todas las entradas maestras para este tipo de RAM? Esta acción es irreversible.")) {
return; return;
} }
const toastId = toast.loading('Eliminando grupo de módulos...'); const toastId = toast.loading('Eliminando grupo de módulos...');
try { try {
// El servicio ahora espera el objeto del grupo
await adminService.deleteRamComponent({ await adminService.deleteRamComponent({
fabricante: ramGroup.fabricante, fabricante: ramGroup.fabricante,
tamano: ramGroup.tamano, tamano: ramGroup.tamano,
velocidad: ramGroup.velocidad velocidad: ramGroup.velocidad
}); });
setValores(prev => prev.filter(v => { setValores(prev => prev.filter(v => {
const currentRam = v as RamValue; const currentRam = v as RamValue;
return !(currentRam.fabricante === ramGroup.fabricante && return !(currentRam.fabricante === ramGroup.fabricante &&
currentRam.tamano === ramGroup.tamano && currentRam.tamano === ramGroup.tamano &&
currentRam.velocidad === ramGroup.velocidad); currentRam.velocidad === ramGroup.velocidad);
})); }));
toast.success("Grupo de módulos de RAM eliminado.", { id: toastId }); toast.success("Grupo de módulos de RAM eliminado.", { id: toastId });
} catch (error) { } catch (error) {
if (error instanceof Error) toast.error(error.message, { id: toastId }); if (error instanceof Error) toast.error(error.message, { id: toastId });
} }
}; }, []);
const handleDeleteTexto = async (valor: string) => { const handleDeleteTexto = useCallback(async (valor: string) => {
if (!window.confirm(`Este valor ya no está en uso. ¿Quieres eliminarlo de la base de datos maestra?`)) { if (!window.confirm(`Este valor ya no está en uso. ¿Quieres eliminarlo de la base de datos maestra?`)) {
return; return;
} }
const toastId = toast.loading('Eliminando valor...'); const toastId = toast.loading('Eliminando valor...');
try { try {
@@ -93,103 +105,159 @@ const GestionComponentes = () => {
setValores(prev => prev.filter(v => (v as TextValue).valor !== valor)); setValores(prev => prev.filter(v => (v as TextValue).valor !== valor));
toast.success("Valor eliminado.", { id: toastId }); toast.success("Valor eliminado.", { id: toastId });
} catch (error) { } catch (error) {
if (error instanceof Error) toast.error(error.message, { id: toastId }); if (error instanceof Error) toast.error(error.message, { id: toastId });
} }
}; }, [componentType]);
const renderValor = (item: TextValue | RamValue) => { const renderValor = useCallback((item: ComponentValue) => {
if (componentType === 'ram') { if (componentType === 'ram') {
const ram = item as RamValue; const ram = item as RamValue;
return `${ram.fabricante || 'Desconocido'} ${ram.tamano}GB ${ram.velocidad ? ram.velocidad + 'MHz' : ''}`; return `${ram.fabricante || 'Desconocido'} ${ram.tamano}GB ${ram.velocidad ? ram.velocidad + 'MHz' : ''}`;
} }
return (item as TextValue).valor; return (item as TextValue).valor;
}; }, [componentType]);
const columns = useMemo(() => [
{
header: 'Valor Registrado',
id: 'valor',
accessorFn: (row: ComponentValue) => renderValor(row),
},
{
header: 'Nº de Equipos',
accessorKey: 'conteo',
},
{
header: 'Acciones',
id: 'acciones',
cell: ({ row }: { row: { original: ComponentValue } }) => {
const item = row.original;
return (
<div style={{ display: 'flex', gap: '5px' }}>
{componentType === 'ram' ? (
<button
onClick={() => handleDeleteRam(item as RamValue)}
className={styles.deleteUserButton}
style={{ fontSize: '0.9em', border: '1px solid', borderRadius: '4px', padding: '4px 8px', cursor: item.conteo > 0 ? 'not-allowed' : 'pointer', opacity: item.conteo > 0 ? 0.5 : 1 }}
disabled={item.conteo > 0}
title={item.conteo > 0 ? `En uso por ${item.conteo} equipo(s)` : 'Eliminar este grupo de módulos maestros'}
>
🗑 Eliminar
</button>
) : (
<>
<button onClick={() => handleOpenModal((item as TextValue).valor)} className={styles.tableButton}>
Unificar
</button>
<button
onClick={() => handleDeleteTexto((item as TextValue).valor)}
className={styles.deleteUserButton}
style={{ fontSize: '0.9em', border: '1px solid', borderRadius: '4px', padding: '4px 8px', cursor: item.conteo > 0 ? 'not-allowed' : 'pointer', opacity: item.conteo > 0 ? 0.5 : 1 }}
disabled={item.conteo > 0}
title={item.conteo > 0 ? `En uso por ${item.conteo} equipo(s). Unifique primero.` : 'Eliminar este valor'}
>
🗑 Eliminar
</button>
</>
)}
</div>
);
}
}
], [componentType, renderValor, handleDeleteRam, handleDeleteTexto, handleOpenModal]);
const table = useReactTable({
data: valores,
columns,
state: {
sorting,
globalFilter,
},
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
});
return ( return (
<div> <div>
<h2>Gestión de Componentes Maestros</h2> <h2>Gestión de Componentes Maestros ({table.getFilteredRowModel().rows.length})</h2>
<p>Unifica valores inconsistentes y elimina registros no utilizados.</p> <p>Unifica valores inconsistentes y elimina registros no utilizados.</p>
<p style={{ fontSize: '14px', fontWeight: 'bold' }}>** La Tabla permite ordenar por multiple columnas manteniendo shift al hacer click en la cabecera. **</p>
<div style={{ marginBottom: '1.5rem' }}> <div className={styles.controlsContainer}>
<label><strong>Selecciona un tipo de componente:</strong></label> <input
<select value={componentType} onChange={e => setComponentType(e.target.value)} className={styles.sectorSelect} style={{marginLeft: '10px'}}> type="text"
<option value="os">Sistema Operativo</option> placeholder="Filtrar registros..."
<option value="cpu">CPU</option> value={globalFilter}
<option value="motherboard">Motherboard</option> onChange={(e) => setGlobalFilter(e.target.value)}
<option value="architecture">Arquitectura</option> className={styles.searchInput}
<option value="ram">Memorias RAM</option> style={{ width: '300px' }}
</select> />
</div> <label><strong>Tipo de componente:</strong></label>
<select value={componentType} onChange={e => setComponentType(e.target.value)} className={styles.sectorSelect}>
<option value="os">Sistema Operativo</option>
<option value="cpu">CPU</option>
<option value="motherboard">Motherboard</option>
<option value="architecture">Arquitectura</option>
<option value="ram">Memorias RAM</option>
</select>
</div>
{isLoading ? ( {isLoading ? (
<p>Cargando...</p> <p>Cargando...</p>
) : ( ) : (
<table className={styles.table}> <div style={{ overflowX: 'auto', border: '1px solid #dee2e6', borderRadius: '8px', marginTop: '1rem' }}>
<thead> <table className={styles.table}>
<tr> <thead>
<th className={styles.th}>Valor Registrado</th> {table.getHeaderGroups().map(headerGroup => (
<th className={styles.th} style={{width: '150px'}}> de Equipos</th> <tr key={headerGroup.id}>
<th className={styles.th} style={{width: '200px'}}>Acciones</th> {headerGroup.headers.map(header => (
</tr> <th key={header.id} className={styles.th} onClick={header.column.getToggleSortingHandler()}>
</thead> {flexRender(header.column.columnDef.header, header.getContext())}
<tbody> {header.column.getIsSorted() && (
{valores.map((item) => ( <span className={styles.sortIndicator}>
<tr key={componentType === 'ram' ? `${(item as RamValue).fabricante}-${(item as RamValue).tamano}-${(item as RamValue).velocidad}` : (item as TextValue).valor} className={styles.tr}> {header.column.getIsSorted() === 'asc' ? '⇑' : '⇓'}
<td className={styles.td}>{renderValor(item)}</td> </span>
<td className={styles.td}>{item.conteo}</td> )}
<td className={styles.td}> </th>
<div style={{display: 'flex', gap: '5px'}}> ))}
{componentType === 'ram' ? ( </tr>
<button ))}
onClick={() => handleDeleteRam(item as RamValue)} </thead>
className={styles.deleteUserButton} <tbody>
style={{fontSize: '0.9em', border: '1px solid', borderRadius: '4px', padding: '4px 8px', cursor: item.conteo > 0 ? 'not-allowed' : 'pointer', opacity: item.conteo > 0 ? 0.5 : 1}} {table.getRowModel().rows.map(row => (
disabled={item.conteo > 0} <tr key={row.id} className={styles.tr}>
title={item.conteo > 0 ? `En uso por ${item.conteo} equipo(s)` : 'Eliminar este grupo de módulos maestros'} {row.getVisibleCells().map(cell => (
> <td key={cell.id} className={styles.td}>
🗑 Eliminar {flexRender(cell.column.columnDef.cell, cell.getContext())}
</button> </td>
) : ( ))}
<> </tr>
<button onClick={() => handleOpenModal((item as TextValue).valor)} className={styles.tableButton}> ))}
Unificar </tbody>
</button> </table>
<button
onClick={() => handleDeleteTexto((item as TextValue).valor)}
className={styles.deleteUserButton}
style={{fontSize: '0.9em', border: '1px solid', borderRadius: '4px', padding: '4px 8px', cursor: item.conteo > 0 ? 'not-allowed' : 'pointer', opacity: item.conteo > 0 ? 0.5 : 1}}
disabled={item.conteo > 0}
title={item.conteo > 0 ? `En uso por ${item.conteo} equipo(s). Unifique primero.` : 'Eliminar este valor'}
>
🗑 Eliminar
</button>
</>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
{isModalOpen && (
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<h3>Unificar Valor</h3>
<p>Se reemplazarán todas las instancias de:</p>
<strong style={{ display: 'block', marginBottom: '1rem', background: '#e9ecef', padding: '8px', borderRadius: '4px' }}>{valorAntiguo}</strong>
<label>Por el nuevo valor:</label>
<input type="text" value={valorNuevo} onChange={e => setValorNuevo(e.target.value)} className={styles.modalInput} />
<div className={styles.modalActions}>
<button onClick={handleUnificar} className={`${styles.btn} ${styles.btnPrimary}`} disabled={!valorNuevo.trim() || valorNuevo === valorAntiguo}>Unificar</button>
<button onClick={() => setIsModalOpen(false)} className={`${styles.btn} ${styles.btnSecondary}`}>Cancelar</button>
</div>
</div>
</div>
)}
</div> </div>
); )}
{isModalOpen && (
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<h3>Unificar Valor</h3>
<p>Se reemplazarán todas las instancias de:</p>
<strong style={{ display: 'block', marginBottom: '1rem', background: '#e9ecef', padding: '8px', borderRadius: '4px' }}>{valorAntiguo}</strong>
<label>Por el nuevo valor:</label>
<input type="text" value={valorNuevo} onChange={e => setValorNuevo(e.target.value)} className={styles.modalInput} />
<div className={styles.modalActions}>
<button onClick={handleUnificar} className={`${styles.btn} ${styles.btnPrimary}`} disabled={!valorNuevo.trim() || valorNuevo === valorAntiguo}>Unificar</button>
<button onClick={() => setIsModalOpen(false)} className={`${styles.btn} ${styles.btnSecondary}`}>Cancelar</button>
</div>
</div>
</div>
)}
</div>
);
}; };
export default GestionComponentes; export default GestionComponentes;

View File

@@ -2,7 +2,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
useReactTable, getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable, getCoreRowModel, getFilteredRowModel, getSortedRowModel,
getPaginationRowModel, flexRender, type CellContext getPaginationRowModel, flexRender, type CellContext,
type ColumnDef
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { Tooltip } from 'react-tooltip'; import { Tooltip } from 'react-tooltip';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -125,13 +126,18 @@ const SimpleTable = () => {
}; };
const handleSave = async () => { const handleSave = async () => {
if (!modalData || !modalData.sector) return; if (!modalData) return;
const toastId = toast.loading('Guardando...'); const toastId = toast.loading('Guardando...');
try { try {
await equipoService.updateSector(modalData.id, modalData.sector.id); const sectorId = modalData.sector?.id ?? 0;
const updatedData = data.map(e => e.id === modalData.id ? { ...e, sector: modalData.sector } : e); await equipoService.updateSector(modalData.id, sectorId);
setData(updatedData); const equipoActualizado = { ...modalData, sector_id: modalData.sector?.id };
setFilteredData(updatedData); 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 }); toast.success('Sector actualizado.', { id: toastId });
setModalData(null); setModalData(null);
} catch (error) { } catch (error) {
@@ -143,28 +149,57 @@ const SimpleTable = () => {
if (!modalPasswordData) return; if (!modalPasswordData) return;
const toastId = toast.loading('Actualizando...'); const toastId = toast.loading('Actualizando...');
try { try {
const updatedUser = await usuarioService.updatePassword(modalPasswordData.id, password); await usuarioService.updatePassword(modalPasswordData.id, password);
const updatedData = data.map(equipo => ({
...equipo, const usernameToUpdate = modalPasswordData.username;
usuarios: equipo.usuarios?.map(user => user.id === updatedUser.id ? { ...user, password } : user)
})); const newData = data.map(equipo => {
setData(updatedData); if (!equipo.usuarios.some(u => u.username === usernameToUpdate)) {
setFilteredData(updatedData); return equipo;
toast.success(`Contraseña actualizada.`, { id: toastId }); }
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); setModalPasswordData(null);
} catch (error) { } catch (error) {
if (error instanceof Error) toast.error(error.message, { id: toastId }); if (error instanceof Error) toast.error(error.message, { id: toastId });
} }
}; };
const handleRemoveUser = async (hostname: string, username: string) => { const handleRemoveUser = async (hostname: string, username: string) => {
if (!window.confirm(`¿Quitar a ${username} de este equipo?`)) return; if (!window.confirm(`¿Quitar a ${username} de este equipo?`)) return;
const toastId = toast.loading(`Quitando a ${username}...`); const toastId = toast.loading(`Quitando a ${username}...`);
try { try {
await usuarioService.removeUserFromEquipo(hostname, username); 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); setData(updateFunc);
setFilteredData(updateFunc); setFilteredData(updateFunc);
if (selectedEquipo && equipoActualizado && selectedEquipo.id === equipoActualizado.id) {
setSelectedEquipo(equipoActualizado);
}
toast.success(`${username} quitado.`, { id: toastId }); toast.success(`${username} quitado.`, { id: toastId });
} catch (error) { } catch (error) {
if (error instanceof Error) toast.error(error.message, { id: toastId }); 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 }) => { 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; if (!window.confirm('¿Estás seguro de que quieres eliminar esta asociación manual?')) return;
const toastId = toast.loading('Eliminando asociación...'); const toastId = toast.loading('Eliminando asociación...');
try { try {
let successMessage = ''; let successMessage = '';
@@ -204,7 +238,6 @@ const SimpleTable = () => {
} else { } else {
throw new Error('Tipo de asociación no válido'); throw new Error('Tipo de asociación no válido');
} }
const updateState = (prevEquipos: Equipo[]) => prevEquipos.map(equipo => { const updateState = (prevEquipos: Equipo[]) => prevEquipos.map(equipo => {
if (equipo.id !== selectedEquipo?.id) return equipo; if (equipo.id !== selectedEquipo?.id) return equipo;
let updatedEquipo = { ...equipo }; let updatedEquipo = { ...equipo };
@@ -217,11 +250,9 @@ const SimpleTable = () => {
} }
return updatedEquipo; return updatedEquipo;
}); });
setData(updateState); setData(updateState);
setFilteredData(updateState); setFilteredData(updateState);
setSelectedEquipo(prev => prev ? updateState([prev])[0] : null); setSelectedEquipo(prev => prev ? updateState([prev])[0] : null);
if (selectedEquipo) { if (selectedEquipo) {
await refreshHistory(selectedEquipo.hostname); await refreshHistory(selectedEquipo.hostname);
} }
@@ -248,13 +279,10 @@ const SimpleTable = () => {
const toastId = toast.loading('Guardando cambios...'); const toastId = toast.loading('Guardando cambios...');
try { try {
const equipoActualizadoDesdeBackend = await equipoService.updateManual(id, equipoEditado); const equipoActualizadoDesdeBackend = await equipoService.updateManual(id, equipoEditado);
const updateState = (prev: Equipo[]) => const updateState = (prev: Equipo[]) => prev.map(e => e.id === id ? equipoActualizadoDesdeBackend : e);
prev.map(e => e.id === id ? equipoActualizadoDesdeBackend : e);
setData(updateState); setData(updateState);
setFilteredData(updateState); setFilteredData(updateState);
setSelectedEquipo(equipoActualizadoDesdeBackend); setSelectedEquipo(equipoActualizadoDesdeBackend);
toast.success('Equipo actualizado.', { id: toastId }); toast.success('Equipo actualizado.', { id: toastId });
return true; return true;
} catch (error) { } catch (error) {
@@ -275,18 +303,13 @@ const SimpleTable = () => {
default: throw new Error('Tipo de componente no válido'); default: throw new Error('Tipo de componente no válido');
} }
await serviceCall; 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)); 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"); if (!refreshedEquipo) throw new Error("No se pudo recargar el equipo");
const updateState = (prev: Equipo[]) => prev.map(e => e.id === selectedEquipo.id ? refreshedEquipo : e); const updateState = (prev: Equipo[]) => prev.map(e => e.id === selectedEquipo.id ? refreshedEquipo : e);
setData(updateState); setData(updateState);
setFilteredData(updateState); setFilteredData(updateState);
setSelectedEquipo(refreshedEquipo); setSelectedEquipo(refreshedEquipo);
await refreshHistory(selectedEquipo.hostname); await refreshHistory(selectedEquipo.hostname);
toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} añadido.`, { id: toastId }); toast.success(`${type.charAt(0).toUpperCase() + type.slice(1)} añadido.`, { id: toastId });
setAddingComponent(null); setAddingComponent(null);
} catch (error) { } catch (error) {
@@ -294,11 +317,12 @@ const SimpleTable = () => {
} }
}; };
const columns = [ // --- DEFINICIÓN DE COLUMNAS CORREGIDA ---
const columns: ColumnDef<Equipo>[] = [
{ header: "ID", accessorKey: "id", enableHiding: true }, { header: "ID", accessorKey: "id", enableHiding: true },
{ {
header: "Nombre", accessorKey: "hostname", 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: "IP", accessorKey: "ip" },
{ header: "MAC", accessorKey: "mac", enableHiding: true }, { header: "MAC", accessorKey: "mac", enableHiding: true },
@@ -310,7 +334,8 @@ const SimpleTable = () => {
{ header: "Arquitectura", accessorKey: "architecture" }, { header: "Arquitectura", accessorKey: "architecture" },
{ {
header: "Usuarios y Claves", header: "Usuarios y Claves",
cell: ({ row }: CellContext<Equipo, any>) => { cell: (info: CellContext<Equipo, any>) => {
const { row } = info;
const usuarios = row.original.usuarios || []; const usuarios = row.original.usuarios || [];
return ( return (
<div className={styles.userList}> <div className={styles.userList}>
@@ -347,12 +372,13 @@ const SimpleTable = () => {
}, },
{ {
header: "Sector", id: 'sector', accessorFn: (row: Equipo) => row.sector?.nombre || 'Asignar', 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; const sector = row.original.sector;
return ( return (
<div className={styles.sectorContainer}> <div className={styles.sectorContainer}>
<span className={`${styles.sectorName} ${sector ? styles.sectorNameAssigned : styles.sectorNameUnassigned}`}>{sector?.nombre || 'Asignar'}</span> <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> </div>
); );
} }