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.
		
			
				
	
	
		
			202 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			202 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| // backend/Controllers/AdminController.cs
 | |
| using Dapper;
 | |
| using Inventario.API.Data;
 | |
| using Microsoft.AspNetCore.Mvc;
 | |
| 
 | |
| namespace Inventario.API.Controllers
 | |
| {
 | |
|   [ApiController]
 | |
|   [Route("api/[controller]")]
 | |
|   public class AdminController : ControllerBase
 | |
|   {
 | |
|     private readonly DapperContext _context;
 | |
| 
 | |
|     public AdminController(DapperContext context)
 | |
|     {
 | |
|       _context = context;
 | |
|     }
 | |
| 
 | |
|     // --- DTOs para los componentes ---
 | |
|     public class ComponenteValorDto
 | |
|     {
 | |
|       public string Valor { get; set; } = "";
 | |
|       public int Conteo { get; set; }
 | |
|     }
 | |
|     public class UnificarComponenteDto
 | |
|     {
 | |
|       public required string ValorNuevo { get; set; }
 | |
|       public required string ValorAntiguo { get; set; }
 | |
|     }
 | |
| 
 | |
|     public class RamAgrupadaDto
 | |
|     {
 | |
|       public string? Fabricante { get; set; }
 | |
|       public int Tamano { get; set; }
 | |
|       public int? Velocidad { get; set; }
 | |
|       public int Conteo { get; set; }
 | |
|     }
 | |
| 
 | |
|     public class BorrarRamAgrupadaDto
 | |
|     {
 | |
|       public string? Fabricante { get; set; }
 | |
|       public int Tamano { get; set; }
 | |
|       public int? Velocidad { get; set; }
 | |
|     }
 | |
| 
 | |
| 
 | |
|     [HttpGet("componentes/{tipo}")]
 | |
|     public async Task<IActionResult> GetComponenteValores(string tipo)
 | |
|     {
 | |
|       var allowedTypes = new Dictionary<string, string>
 | |
|             {
 | |
|                 { "os", "Os" },
 | |
|                 { "cpu", "Cpu" },
 | |
|                 { "motherboard", "Motherboard" },
 | |
|                 { "architecture", "Architecture" }
 | |
|             };
 | |
| 
 | |
|       if (!allowedTypes.TryGetValue(tipo.ToLower(), out var columnName))
 | |
|       {
 | |
|         return BadRequest("Tipo de componente no válido.");
 | |
|       }
 | |
| 
 | |
|       var query = $@"
 | |
|                 SELECT {columnName} AS Valor, COUNT(*) AS Conteo
 | |
|                 FROM dbo.equipos
 | |
|                 WHERE {columnName} IS NOT NULL AND {columnName} != ''
 | |
|                 GROUP BY {columnName}
 | |
|                 ORDER BY Conteo DESC, Valor ASC;";
 | |
| 
 | |
|       using (var connection = _context.CreateConnection())
 | |
|       {
 | |
|         var valores = await connection.QueryAsync<ComponenteValorDto>(query);
 | |
|         return Ok(valores);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     [HttpPut("componentes/{tipo}/unificar")]
 | |
|     public async Task<IActionResult> UnificarComponenteValores(string tipo, [FromBody] UnificarComponenteDto dto)
 | |
|     {
 | |
|       var allowedTypes = new Dictionary<string, string>
 | |
|             {
 | |
|                 { "os", "Os" },
 | |
|                 { "cpu", "Cpu" },
 | |
|                 { "motherboard", "Motherboard" },
 | |
|                 { "architecture", "Architecture" }
 | |
|             };
 | |
| 
 | |
|       if (!allowedTypes.TryGetValue(tipo.ToLower(), out var columnName))
 | |
|       {
 | |
|         return BadRequest("Tipo de componente no válido.");
 | |
|       }
 | |
| 
 | |
|       if (dto.ValorAntiguo == dto.ValorNuevo)
 | |
|       {
 | |
|         return BadRequest("El valor antiguo y el nuevo no pueden ser iguales.");
 | |
|       }
 | |
| 
 | |
|       var query = $@"
 | |
|                 UPDATE dbo.equipos
 | |
|                 SET {columnName} = @ValorNuevo
 | |
|                 WHERE {columnName} = @ValorAntiguo;";
 | |
| 
 | |
|       using (var connection = _context.CreateConnection())
 | |
|       {
 | |
|         var filasAfectadas = await connection.ExecuteAsync(query, new { dto.ValorNuevo, dto.ValorAntiguo });
 | |
|         return Ok(new { message = $"Se unificaron {filasAfectadas} registros.", filasAfectadas });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // --- Devuelve la RAM agrupada ---
 | |
|     [HttpGet("componentes/ram")]
 | |
|     public async Task<IActionResult> GetComponentesRam()
 | |
|     {
 | |
|       var query = @"
 | |
|             SELECT
 | |
|                 mr.Fabricante,
 | |
|                 mr.Tamano,
 | |
|                 mr.Velocidad,
 | |
|                 COUNT(emr.memoria_ram_id) as Conteo
 | |
|             FROM
 | |
|                 dbo.memorias_ram mr
 | |
|             LEFT JOIN
 | |
|                 dbo.equipos_memorias_ram emr ON mr.id = emr.memoria_ram_id
 | |
|             GROUP BY
 | |
|                 mr.Fabricante,
 | |
|                 mr.Tamano,
 | |
|                 mr.Velocidad
 | |
|             ORDER BY
 | |
|                 Conteo DESC, mr.Fabricante, mr.Tamano;";
 | |
| 
 | |
|       using (var connection = _context.CreateConnection())
 | |
|       {
 | |
|         var valores = await connection.QueryAsync<RamAgrupadaDto>(query);
 | |
|         return Ok(valores);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // --- Elimina un grupo completo ---
 | |
|     [HttpDelete("componentes/ram")]
 | |
|     public async Task<IActionResult> BorrarComponenteRam([FromBody] BorrarRamAgrupadaDto dto)
 | |
|     {
 | |
|       using (var connection = _context.CreateConnection())
 | |
|       {
 | |
|         // Verificación de seguridad: Asegurarse de que el grupo no esté en uso.
 | |
|         var usageQuery = @"
 | |
|                 SELECT COUNT(emr.id)
 | |
|                 FROM dbo.memorias_ram mr
 | |
|                 LEFT JOIN dbo.equipos_memorias_ram emr ON mr.id = emr.memoria_ram_id
 | |
|                 WHERE (mr.Fabricante = @Fabricante OR (mr.Fabricante IS NULL AND @Fabricante IS NULL)) 
 | |
|                   AND mr.Tamano = @Tamano 
 | |
|                   AND (mr.Velocidad = @Velocidad OR (mr.Velocidad IS NULL AND @Velocidad IS NULL));";
 | |
| 
 | |
|         var usageCount = await connection.ExecuteScalarAsync<int>(usageQuery, dto);
 | |
| 
 | |
|         if (usageCount > 0)
 | |
|         {
 | |
|           return Conflict(new { message = $"Este grupo de RAM está en uso por {usageCount} equipo(s) y no puede ser eliminado." });
 | |
|         }
 | |
| 
 | |
|         // Si no está en uso, proceder con la eliminación de todos los registros maestros que coincidan.
 | |
|         var deleteQuery = @"
 | |
|                 DELETE FROM dbo.memorias_ram 
 | |
|                 WHERE (Fabricante = @Fabricante OR (Fabricante IS NULL AND @Fabricante IS NULL)) 
 | |
|                   AND Tamano = @Tamano 
 | |
|                   AND (Velocidad = @Velocidad OR (Velocidad IS NULL AND @Velocidad IS NULL));";
 | |
| 
 | |
|         await connection.ExecuteAsync(deleteQuery, dto);
 | |
|         return NoContent();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     [HttpDelete("componentes/{tipo}/{valor}")]
 | |
|     public async Task<IActionResult> BorrarComponenteTexto(string tipo, string valor)
 | |
|     {
 | |
|       var allowedTypes = new Dictionary<string, string>
 | |
|         {
 | |
|             { "os", "Os" },
 | |
|             { "cpu", "Cpu" },
 | |
|             { "motherboard", "Motherboard" },
 | |
|             { "architecture", "Architecture" }
 | |
|         };
 | |
| 
 | |
|       if (!allowedTypes.TryGetValue(tipo.ToLower(), out var columnName))
 | |
|       {
 | |
|         return BadRequest("Tipo de componente no válido.");
 | |
|       }
 | |
| 
 | |
|       using (var connection = _context.CreateConnection())
 | |
|       {
 | |
|         var usageQuery = $"SELECT COUNT(*) FROM dbo.equipos WHERE {columnName} = @Valor;";
 | |
|         var usageCount = await connection.ExecuteScalarAsync<int>(usageQuery, new { Valor = valor });
 | |
| 
 | |
|         if (usageCount > 0)
 | |
|         {
 | |
|           return Conflict(new { message = $"Este valor está en uso por {usageCount} equipo(s) y no puede ser eliminado. Intente unificarlo en su lugar." });
 | |
|         }
 | |
| 
 | |
|         return NoContent();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| } |