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
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo, useCallback } from 'react';
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 { adminService } from '../services/apiService';
@@ -17,14 +25,20 @@ interface RamValue {
conteo: number;
}
type ComponentValue = TextValue | RamValue;
const GestionComponentes = () => {
const [componentType, setComponentType] = useState('os');
const [valores, setValores] = useState<(TextValue | RamValue)[]>([]);
const [valores, setValores] = useState<ComponentValue[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [valorAntiguo, setValorAntiguo] = useState('');
const [valorNuevo, setValorNuevo] = useState('');
// Estados para la tabla (filtrado y ordenamiento)
const [globalFilter, setGlobalFilter] = useState('');
const [sorting, setSorting] = useState<SortingState>([]);
useEffect(() => {
setIsLoading(true);
adminService.getComponentValues(componentType)
@@ -37,11 +51,11 @@ const GestionComponentes = () => {
.finally(() => setIsLoading(false));
}, [componentType]);
const handleOpenModal = (valor: string) => {
const handleOpenModal = useCallback((valor: string) => {
setValorAntiguo(valor);
setValorNuevo(valor);
setIsModalOpen(true);
};
}, []);
const handleUnificar = async () => {
const toastId = toast.loading('Unificando valores...');
@@ -56,36 +70,34 @@ const GestionComponentes = () => {
}
};
// 2. FUNCIÓN DELETE ACTUALIZADA: Ahora maneja un grupo
const handleDeleteRam = async (ramGroup: RamValue) => {
const handleDeleteRam = useCallback(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.")) {
return;
}
const toastId = toast.loading('Eliminando grupo de módulos...');
try {
// El servicio ahora espera el objeto del grupo
await adminService.deleteRamComponent({
fabricante: ramGroup.fabricante,
tamano: ramGroup.tamano,
velocidad: ramGroup.velocidad
fabricante: ramGroup.fabricante,
tamano: ramGroup.tamano,
velocidad: ramGroup.velocidad
});
setValores(prev => prev.filter(v => {
const currentRam = v as RamValue;
return !(currentRam.fabricante === ramGroup.fabricante &&
currentRam.tamano === ramGroup.tamano &&
currentRam.velocidad === ramGroup.velocidad);
const currentRam = v as RamValue;
return !(currentRam.fabricante === ramGroup.fabricante &&
currentRam.tamano === ramGroup.tamano &&
currentRam.velocidad === ramGroup.velocidad);
}));
toast.success("Grupo de módulos de RAM eliminado.", { id: toastId });
} catch (error) {
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?`)) {
return;
return;
}
const toastId = toast.loading('Eliminando valor...');
try {
@@ -93,103 +105,159 @@ const GestionComponentes = () => {
setValores(prev => prev.filter(v => (v as TextValue).valor !== valor));
toast.success("Valor eliminado.", { id: toastId });
} 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') {
const ram = item as RamValue;
return `${ram.fabricante || 'Desconocido'} ${ram.tamano}GB ${ram.velocidad ? ram.velocidad + 'MHz' : ''}`;
}
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 (
<div>
<h2>Gestión de Componentes Maestros</h2>
<p>Unifica valores inconsistentes y elimina registros no utilizados.</p>
<div>
<h2>Gestión de Componentes Maestros ({table.getFilteredRowModel().rows.length})</h2>
<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' }}>
<label><strong>Selecciona un tipo de componente:</strong></label>
<select value={componentType} onChange={e => setComponentType(e.target.value)} className={styles.sectorSelect} style={{marginLeft: '10px'}}>
<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 ? (
<p>Cargando...</p>
) : (
<table className={styles.table}>
<thead>
<tr>
<th className={styles.th}>Valor Registrado</th>
<th className={styles.th} style={{width: '150px'}}> de Equipos</th>
<th className={styles.th} style={{width: '200px'}}>Acciones</th>
</tr>
</thead>
<tbody>
{valores.map((item) => (
<tr key={componentType === 'ram' ? `${(item as RamValue).fabricante}-${(item as RamValue).tamano}-${(item as RamValue).velocidad}` : (item as TextValue).valor} className={styles.tr}>
<td className={styles.td}>{renderValor(item)}</td>
<td className={styles.td}>{item.conteo}</td>
<td className={styles.td}>
<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>
</td>
</tr>
))}
</tbody>
</table>
)}
<div className={styles.controlsContainer}>
<input
type="text"
placeholder="Filtrar registros..."
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
className={styles.searchInput}
style={{ width: '300px' }}
/>
<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>
{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>
)}
{isLoading ? (
<p>Cargando...</p>
) : (
<div style={{ overflowX: 'auto', border: '1px solid #dee2e6', borderRadius: '8px', marginTop: '1rem' }}>
<table className={styles.table}>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id} className={styles.th} onClick={header.column.getToggleSortingHandler()}>
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() && (
<span className={styles.sortIndicator}>
{header.column.getIsSorted() === 'asc' ? '⇑' : '⇓'}
</span>
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id} className={styles.tr}>
{row.getVisibleCells().map(cell => (
<td key={cell.id} className={styles.td}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</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;