Files
Inventario-IT/frontend/src/components/GestionComponentes.tsx
dmolinari 268c1c2bf9 Mejoras integrales en UI, lógica de negocio y auditoría
Este commit introduce una serie de mejoras significativas en toda la aplicación, abordando la experiencia de usuario, la consistencia de los datos, la robustez del backend y la implementación de un historial de cambios completo.

 **Funcionalidades y Mejoras (Features & Enhancements)**

*   **Historial de Auditoría Completo:**
    *   Se implementa el registro en el historial para todas las acciones CRUD manuales: creación de equipos, adición y eliminación de discos, RAM y usuarios.
    *   Los cambios de campos simples (IP, Hostname, etc.) ahora también se registran detalladamente.

*   **Consistencia de Datos Mejorada:**
    *   **RAM:** La selección de RAM en el modal de "Añadir RAM" y la vista de "Administración" ahora agrupan los módulos por especificaciones (Fabricante, Tamaño, Velocidad), eliminando las entradas duplicadas causadas por diferentes `part_number`.
    *   **Arquitectura:** El campo de edición para la arquitectura del equipo se ha cambiado de un input de texto a un selector con las opciones fijas "32 bits" y "64 bits".

*   **Experiencia de Usuario (UX) Optimizada:**
    *   El botón de "Wake On Lan" (WOL) ahora se deshabilita visualmente si el equipo no tiene una dirección MAC registrada.
    *   Se corrige el apilamiento de modales: los sub-modales (Añadir Disco/RAM/Usuario) ahora siempre aparecen por encima del modal principal de detalles y bloquean su cierre.
    *   El historial de cambios se actualiza en tiempo real en la interfaz después de añadir o eliminar un componente, sin necesidad de cerrar y reabrir el modal.

🐛 **Correcciones (Bug Fixes)**

*   **Actualización de Estado en Vivo:** Al añadir/eliminar un módulo de RAM, los campos "RAM Instalada" y "Última Actualización" ahora se recalculan en el backend y se actualizan instantáneamente en el frontend.
*   **Historial de Sectores Legible:** Se corrige el registro del historial para que al cambiar un sector se guarde el *nombre* del sector (ej. "Técnica") en lugar de su ID numérico.
*   **Formulario de Edición:** El dropdown de "Sector" en el modo de edición ahora preselecciona correctamente el sector asignado actualmente al equipo.
*   **Error Crítico al Añadir RAM:** Se soluciona un error del servidor (`Sequence contains more than one element`) que ocurría al añadir manualmente un tipo de RAM que ya existía con múltiples `part_number`. Se reemplazó `QuerySingleOrDefaultAsync` por `QueryFirstOrDefaultAsync` para mayor robustez.
*   **Eliminación Segura:** Se impide la eliminación de un sector si este tiene equipos asignados, protegiendo la integridad de los datos.

♻️ **Refactorización (Refactoring)**

*   **Servicio de API Centralizado:** Toda la lógica de llamadas `fetch` del frontend ha sido extraída de los componentes y centralizada en un único servicio (`apiService.ts`), mejorando drásticamente la mantenibilidad y organización del código.
*   **Optimización de Renders:** Se ha optimizado el rendimiento de los modales mediante el uso del hook `useCallback` para memorizar funciones que se pasan como props.
*   **Nulabilidad en C#:** Se han resuelto múltiples advertencias de compilación (`CS8620`) en el backend al especificar explícitamente los tipos de referencia anulables (`string?`), mejorando la seguridad de tipos del código.
2025-10-08 13:27:44 -03:00

195 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// frontend/src/components/GestionComponentes.tsx
import { useState, useEffect } from 'react';
import toast from 'react-hot-toast';
import styles from './SimpleTable.module.css';
import { adminService } from '../services/apiService';
// Interfaces para los diferentes tipos de datos
interface TextValue {
valor: string;
conteo: number;
}
interface RamValue {
fabricante?: string;
tamano: number;
velocidad?: number;
conteo: number;
}
const GestionComponentes = () => {
const [componentType, setComponentType] = useState('os');
const [valores, setValores] = useState<(TextValue | RamValue)[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [valorAntiguo, setValorAntiguo] = useState('');
const [valorNuevo, setValorNuevo] = useState('');
useEffect(() => {
setIsLoading(true);
adminService.getComponentValues(componentType)
.then(data => {
setValores(data);
})
.catch(_err => {
toast.error(`No se pudieron cargar los datos de ${componentType}.`);
})
.finally(() => setIsLoading(false));
}, [componentType]);
const handleOpenModal = (valor: string) => {
setValorAntiguo(valor);
setValorNuevo(valor);
setIsModalOpen(true);
};
const handleUnificar = async () => {
const toastId = toast.loading('Unificando valores...');
try {
await adminService.unifyComponentValues(componentType, valorAntiguo, valorNuevo);
const refreshedData = await adminService.getComponentValues(componentType);
setValores(refreshedData);
toast.success('Valores unificados correctamente.', { id: toastId });
setIsModalOpen(false);
} catch (error) {
if (error instanceof Error) toast.error(error.message, { id: toastId });
}
};
// 2. FUNCIÓN DELETE ACTUALIZADA: Ahora maneja un grupo
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.")) {
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
});
setValores(prev => prev.filter(v => {
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) => {
if (!window.confirm(`Este valor ya no está en uso. ¿Quieres eliminarlo de la base de datos maestra?`)) {
return;
}
const toastId = toast.loading('Eliminando valor...');
try {
await adminService.deleteTextComponent(componentType, valor);
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 });
}
};
const renderValor = (item: TextValue | RamValue) => {
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;
};
return (
<div>
<h2>Gestión de Componentes Maestros</h2>
<p>Unifica valores inconsistentes y elimina registros no utilizados.</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>
)}
{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;