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.
This commit is contained in:
@@ -16,12 +16,33 @@ namespace Inventario.API.Controllers
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// DTO para devolver los valores y su conteo
|
||||
// --- 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)
|
||||
@@ -53,13 +74,6 @@ namespace Inventario.API.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
// DTO para la petición de unificación
|
||||
public class UnificarComponenteDto
|
||||
{
|
||||
public required string ValorNuevo { get; set; }
|
||||
public required string ValorAntiguo { get; set; }
|
||||
}
|
||||
|
||||
[HttpPut("componentes/{tipo}/unificar")]
|
||||
public async Task<IActionResult> UnificarComponenteValores(string tipo, [FromBody] UnificarComponenteDto dto)
|
||||
{
|
||||
@@ -93,63 +107,64 @@ namespace Inventario.API.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
// DTO para devolver los valores de RAM y su conteo
|
||||
public class RamMaestraDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? Part_number { get; set; }
|
||||
public string? Fabricante { get; set; }
|
||||
public int Tamano { get; set; }
|
||||
public int? Velocidad { get; set; }
|
||||
public int Conteo { get; set; }
|
||||
}
|
||||
|
||||
// --- Devuelve la RAM agrupada ---
|
||||
[HttpGet("componentes/ram")]
|
||||
public async Task<IActionResult> GetComponentesRam()
|
||||
{
|
||||
var query = @"
|
||||
SELECT
|
||||
mr.Id, mr.part_number as PartNumber, mr.Fabricante, mr.Tamano, mr.Velocidad,
|
||||
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.Id, mr.part_number, mr.Fabricante, mr.Tamano, mr.Velocidad
|
||||
mr.Fabricante,
|
||||
mr.Tamano,
|
||||
mr.Velocidad
|
||||
ORDER BY
|
||||
Conteo DESC, mr.Fabricante, mr.Tamano;";
|
||||
|
||||
using (var connection = _context.CreateConnection())
|
||||
{
|
||||
var valores = await connection.QueryAsync<RamMaestraDto>(query);
|
||||
var valores = await connection.QueryAsync<RamAgrupadaDto>(query);
|
||||
return Ok(valores);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("componentes/ram/{id}")]
|
||||
public async Task<IActionResult> BorrarComponenteRam(int id)
|
||||
// --- Elimina un grupo completo ---
|
||||
[HttpDelete("componentes/ram")]
|
||||
public async Task<IActionResult> BorrarComponenteRam([FromBody] BorrarRamAgrupadaDto dto)
|
||||
{
|
||||
using (var connection = _context.CreateConnection())
|
||||
{
|
||||
// 1. Verificación de seguridad: Asegurarse de que el módulo no esté en uso.
|
||||
var usageQuery = "SELECT COUNT(*) FROM dbo.equipos_memorias_ram WHERE memoria_ram_id = @Id;";
|
||||
var usageCount = await connection.ExecuteScalarAsync<int>(usageQuery, new { Id = id });
|
||||
// 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($"Este módulo de RAM está en uso por {usageCount} equipo(s) y no puede ser eliminado.");
|
||||
return Conflict(new { message = $"Este grupo de RAM está en uso por {usageCount} equipo(s) y no puede ser eliminado." });
|
||||
}
|
||||
|
||||
// 2. Si no está en uso, proceder con la eliminación.
|
||||
var deleteQuery = "DELETE FROM dbo.memorias_ram WHERE Id = @Id;";
|
||||
var filasAfectadas = await connection.ExecuteAsync(deleteQuery, new { Id = id });
|
||||
|
||||
if (filasAfectadas == 0)
|
||||
{
|
||||
return NotFound("Módulo de RAM no encontrado.");
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -172,19 +187,14 @@ namespace Inventario.API.Controllers
|
||||
|
||||
using (var connection = _context.CreateConnection())
|
||||
{
|
||||
// 1. Verificación de seguridad: Asegurarse de que el valor no esté en uso.
|
||||
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($"Este valor está en uso por {usageCount} equipo(s) y no puede ser eliminado. Intente unificarlo en su lugar.");
|
||||
return Conflict(new { message = $"Este valor está en uso por {usageCount} equipo(s) y no puede ser eliminado. Intente unificarlo en su lugar." });
|
||||
}
|
||||
|
||||
// Esta parte es más conceptual. Un componente de texto no existe en una tabla maestra,
|
||||
// por lo que no hay nada que "eliminar". El hecho de que el conteo sea 0 significa
|
||||
// que ya no existe en la práctica. Devolvemos éxito para confirmar esto.
|
||||
// Si tuviéramos tablas maestras (ej: dbo.sistemas_operativos), aquí iría la consulta DELETE.
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user