From 80210e5d4c8c41b94acb737e8a8d9935a2ef21b6 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Thu, 2 Oct 2025 15:32:23 -0300 Subject: [PATCH] feat: Controladores con operaciones CRUD completas --- backend/Controllers/DiscosController.cs | 114 ++++++++++ backend/Controllers/EquiposController.cs | 205 ++++++++++++++++++ backend/Controllers/MemoriasRamController.cs | 155 +++++++++++++ backend/Controllers/SectoresController.cs | 106 +++++++-- backend/Controllers/UsuariosController.cs | 123 +++++++++++ backend/Helpers/HistorialHelper.cs | 29 +++ .../net9.0/Inventario.API.AssemblyInfo.cs | 2 +- 7 files changed, 719 insertions(+), 15 deletions(-) create mode 100644 backend/Controllers/DiscosController.cs create mode 100644 backend/Controllers/EquiposController.cs create mode 100644 backend/Controllers/MemoriasRamController.cs create mode 100644 backend/Controllers/UsuariosController.cs create mode 100644 backend/Helpers/HistorialHelper.cs diff --git a/backend/Controllers/DiscosController.cs b/backend/Controllers/DiscosController.cs new file mode 100644 index 0000000..6348b40 --- /dev/null +++ b/backend/Controllers/DiscosController.cs @@ -0,0 +1,114 @@ +using Dapper; +using Inventario.API.Data; +using Inventario.API.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Inventario.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class DiscosController : ControllerBase + { + private readonly DapperContext _context; + + public DiscosController(DapperContext context) + { + _context = context; + } + + // --- GET /api/discos --- + [HttpGet] + public async Task Consultar() + { + var query = "SELECT Id, Mediatype, Size FROM dbo.discos;"; + using (var connection = _context.CreateConnection()) + { + var discos = await connection.QueryAsync(query); + return Ok(discos); + } + } + + // --- GET /api/discos/{id} --- + [HttpGet("{id}")] + public async Task ConsultarDetalle(int id) + { + var query = "SELECT Id, Mediatype, Size FROM dbo.discos WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + var disco = await connection.QuerySingleOrDefaultAsync(query, new { Id = id }); + if (disco == null) + { + return NotFound("Disco no encontrado."); + } + return Ok(disco); + } + } + + // --- POST /api/discos --- + // Replica la lógica de recibir uno o varios discos y crear solo los que no existen. + [HttpPost] + public async Task Ingresar([FromBody] List discos) + { + var queryCheck = "SELECT * FROM dbo.discos WHERE Mediatype = @Mediatype AND Size = @Size;"; + var queryInsert = "INSERT INTO dbo.discos (Mediatype, Size) VALUES (@Mediatype, @Size); SELECT CAST(SCOPE_IDENTITY() as int);"; + + var resultados = new List(); + + using (var connection = _context.CreateConnection()) + { + foreach (var disco in discos) + { + var existente = await connection.QuerySingleOrDefaultAsync(queryCheck, new { disco.Mediatype, disco.Size }); + + if (existente == null) + { + var nuevoId = await connection.ExecuteScalarAsync(queryInsert, new { disco.Mediatype, disco.Size }); + var nuevoDisco = new Disco { Id = nuevoId, Mediatype = disco.Mediatype, Size = disco.Size }; + resultados.Add(new { action = "created", registro = nuevoDisco }); + } + else + { + // Opcional: podrías añadirlo si quieres saber cuáles ya existían + // resultados.Add(new { action = "exists", registro = existente }); + } + } + } + // Devolvemos HTTP 200 OK con la lista de los discos que se crearon. + return Ok(resultados); + } + + // --- PUT /api/discos/{id} --- + [HttpPut("{id}")] + public async Task Actualizar(int id, [FromBody] Disco disco) + { + var query = "UPDATE dbo.discos SET Mediatype = @Mediatype, Size = @Size WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + var filasAfectadas = await connection.ExecuteAsync(query, new { disco.Mediatype, disco.Size, Id = id }); + if (filasAfectadas == 0) + { + return NotFound("Disco no encontrado."); + } + + var discoActualizado = new Disco { Id = id, Mediatype = disco.Mediatype, Size = disco.Size }; + return Ok(discoActualizado); + } + } + + // --- DELETE /api/discos/{id} --- + [HttpDelete("{id}")] + public async Task Borrar(int id) + { + var query = "DELETE FROM dbo.discos WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + var filasAfectadas = await connection.ExecuteAsync(query, new { Id = id }); + if (filasAfectadas == 0) + { + return NotFound("Disco no encontrado."); + } + return NoContent(); + } + } + } +} \ No newline at end of file diff --git a/backend/Controllers/EquiposController.cs b/backend/Controllers/EquiposController.cs new file mode 100644 index 0000000..bb41b47 --- /dev/null +++ b/backend/Controllers/EquiposController.cs @@ -0,0 +1,205 @@ +using Dapper; +using Inventario.API.Data; +using Inventario.API.Helpers; +using Inventario.API.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Inventario.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class EquiposController : ControllerBase + { + private readonly DapperContext _context; + + public EquiposController(DapperContext context) + { + _context = context; + } + + // --- GET /api/equipos --- + // Consulta todos los equipos con sus relaciones + [HttpGet] + public async Task Consultar() + { + // Query para traer todo en una sola llamada a la BD + var query = @" + SELECT + e.*, + s.Id as SectorId, s.Nombre as SectorNombre, + u.Id as UsuarioId, u.Username, u.Password, + d.Id as DiscoId, d.Mediatype, d.Size, + mr.Id as MemoriaRamId, mr.part_number as PartNumber, mr.Fabricante, mr.Tamano, mr.Velocidad, emr.Slot + FROM dbo.equipos e + LEFT JOIN dbo.sectores s ON e.sector_id = s.id + LEFT JOIN dbo.usuarios_equipos ue ON e.id = ue.equipo_id + LEFT JOIN dbo.usuarios u ON ue.usuario_id = u.id + LEFT JOIN dbo.equipos_discos ed ON e.id = ed.equipo_id + LEFT JOIN dbo.discos d ON ed.disco_id = d.id + LEFT JOIN dbo.equipos_memorias_ram emr ON e.id = emr.equipo_id + LEFT JOIN dbo.memorias_ram mr ON emr.memoria_ram_id = mr.id;"; + + using (var connection = _context.CreateConnection()) + { + var equipoDict = new Dictionary(); + + await connection.QueryAsync( + query, (equipo, sector, usuario, disco, memoria) => + { + if (!equipoDict.TryGetValue(equipo.Id, out var equipoActual)) + { + equipoActual = equipo; + equipoActual.Sector = sector; + equipoDict.Add(equipoActual.Id, equipoActual); + } + if (usuario != null && !equipoActual.Usuarios.Any(u => u.Id == usuario.Id)) + equipoActual.Usuarios.Add(usuario); + if (disco != null && !equipoActual.Discos.Any(d => d.Id == disco.Id)) + equipoActual.Discos.Add(disco); + if (memoria != null && !equipoActual.MemoriasRam.Any(m => m.Id == memoria.Id && m.Slot == memoria.Slot)) + equipoActual.MemoriasRam.Add(memoria); + + return equipoActual; + }, + splitOn: "SectorId,UsuarioId,DiscoId,MemoriaRamId" + ); + return Ok(equipoDict.Values); + } + } + + // --- GET /api/equipos/{hostname} --- + [HttpGet("{hostname}")] + public async Task ConsultarDetalle(string hostname) + { + var query = @"SELECT + e.*, + s.Id as SectorId, s.Nombre as SectorNombre, + u.Id as UsuarioId, u.Username, u.Password + FROM dbo.equipos e + LEFT JOIN dbo.sectores s ON e.sector_id = s.id + LEFT JOIN dbo.usuarios_equipos ue ON e.id = ue.equipo_id + LEFT JOIN dbo.usuarios u ON ue.usuario_id = u.id + WHERE e.Hostname = @Hostname;"; + + using (var connection = _context.CreateConnection()) + { + var equipoDict = new Dictionary(); + var equipo = (await connection.QueryAsync( + query, (e, sector, usuario) => + { + if (!equipoDict.TryGetValue(e.Id, out var equipoActual)) + { + equipoActual = e; + equipoActual.Sector = sector; + equipoDict.Add(equipoActual.Id, equipoActual); + } + if (usuario != null && !equipoActual.Usuarios.Any(u => u.Id == usuario.Id)) + equipoActual.Usuarios.Add(usuario); + + return equipoActual; + }, + new { Hostname = hostname }, + splitOn: "SectorId,UsuarioId" + )).FirstOrDefault(); + + if (equipo == null) return NotFound("Equipo no encontrado."); + + return Ok(equipo); + } + } + + // --- POST /api/equipos/{hostname} --- + [HttpPost("{hostname}")] + public async Task Ingresar(string hostname, [FromBody] Equipo equipoData) + { + var findQuery = "SELECT * FROM dbo.equipos WHERE Hostname = @Hostname;"; + + using (var connection = _context.CreateConnection()) + { + var equipoExistente = await connection.QuerySingleOrDefaultAsync(findQuery, new { Hostname = hostname }); + + if (equipoExistente == null) + { + // Crear + var insertQuery = @"INSERT INTO dbo.equipos (Hostname, Ip, Mac, Motherboard, Cpu, Ram_installed, Ram_slots, Os, Architecture) + VALUES (@Hostname, @Ip, @Mac, @Motherboard, @Cpu, @Ram_installed, @Ram_slots, @Os, @Architecture); + SELECT CAST(SCOPE_IDENTITY() as int);"; + var nuevoId = await connection.ExecuteScalarAsync(insertQuery, equipoData); + equipoData.Id = nuevoId; + return CreatedAtAction(nameof(ConsultarDetalle), new { hostname = equipoData.Hostname }, equipoData); + } + else + { + // Actualizar y registrar historial + var cambios = new Dictionary(); + + // Comparamos campos para registrar en historial + if (equipoData.Ip != equipoExistente.Ip) cambios["ip"] = (equipoExistente.Ip, equipoData.Ip); + if (equipoData.Mac != equipoExistente.Mac) cambios["mac"] = (equipoExistente.Mac ?? "", equipoData.Mac ?? ""); + + var updateQuery = @"UPDATE dbo.equipos SET Ip = @Ip, Mac = @Mac, Motherboard = @Motherboard, + Cpu = @Cpu, Ram_installed = @Ram_installed, Ram_slots = @Ram_slots, Os = @Os, Architecture = @Architecture + WHERE Hostname = @Hostname;"; + await connection.ExecuteAsync(updateQuery, equipoData); + + if (cambios.Count > 0) + { + await HistorialHelper.RegistrarCambios(_context, equipoExistente.Id, cambios); + } + + return Ok(equipoData); + } + } + } + + // --- PUT /api/equipos/{id} --- + [HttpPut("{id}")] + public async Task Actualizar(int id, [FromBody] Equipo equipoData) + { + var updateQuery = @"UPDATE dbo.equipos SET Hostname = @Hostname, Ip = @Ip, Mac = @Mac, Motherboard = @Motherboard, + Cpu = @Cpu, Ram_installed = @Ram_installed, Ram_slots = @Ram_slots, Os = @Os, Architecture = @Architecture + WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + // Asignamos el ID del parámetro de la ruta al objeto que recibimos. + equipoData.Id = id; + // Ahora pasamos el objeto completo a Dapper. + var filasAfectadas = await connection.ExecuteAsync(updateQuery, equipoData); + if (filasAfectadas == 0) return NotFound("Equipo no encontrado."); + return NoContent(); + } + } + + // --- DELETE /api/equipos/{id} --- + [HttpDelete("{id}")] + public async Task Borrar(int id) + { + // La base de datos está configurada con ON DELETE CASCADE, por lo que las relaciones se borrarán automáticamente. + var query = "DELETE FROM dbo.equipos WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + var filasAfectadas = await connection.ExecuteAsync(query, new { Id = id }); + if (filasAfectadas == 0) return NotFound("Equipo no encontrado."); + return NoContent(); + } + } + + // --- GET /api/equipos/{hostname}/historial --- + [HttpGet("{hostname}/historial")] + public async Task ConsultarHistorial(string hostname) + { + var query = @"SELECT h.* FROM dbo.historial_equipos h + JOIN dbo.equipos e ON h.equipo_id = e.id + WHERE e.Hostname = @Hostname + ORDER BY h.fecha_cambio DESC;"; + using (var connection = _context.CreateConnection()) + { + var equipo = await connection.QueryFirstOrDefaultAsync("SELECT Id FROM dbo.equipos WHERE Hostname = @Hostname", new { Hostname = hostname }); + if (equipo == null) return NotFound("Equipo no encontrado."); + + var historial = await connection.QueryAsync(query, new { Hostname = hostname }); + return Ok(new { equipo = hostname, historial }); + } + } + } +} \ No newline at end of file diff --git a/backend/Controllers/MemoriasRamController.cs b/backend/Controllers/MemoriasRamController.cs new file mode 100644 index 0000000..807e500 --- /dev/null +++ b/backend/Controllers/MemoriasRamController.cs @@ -0,0 +1,155 @@ +using Dapper; +using Inventario.API.Data; +using Inventario.API.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Inventario.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class MemoriasRamController : ControllerBase + { + private readonly DapperContext _context; + + public MemoriasRamController(DapperContext context) + { + _context = context; + } + + // --- GET /api/memoriasram --- + [HttpGet] + public async Task Consultar() + { + var query = "SELECT Id, part_number as PartNumber, Fabricante, Tamano, Velocidad FROM dbo.memorias_ram;"; + using (var connection = _context.CreateConnection()) + { + var memorias = await connection.QueryAsync(query); + return Ok(memorias); + } + } + + // --- GET /api/memoriasram/{id} --- + [HttpGet("{id}")] + public async Task ConsultarDetalle(int id) + { + var query = "SELECT Id, part_number as PartNumber, Fabricante, Tamano, Velocidad FROM dbo.memorias_ram WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + var memoria = await connection.QuerySingleOrDefaultAsync(query, new { Id = id }); + if (memoria == null) + { + return NotFound("Módulo de memoria RAM no encontrado."); + } + return Ok(memoria); + } + } + + // --- POST /api/memoriasram --- + [HttpPost] + public async Task Ingresar([FromBody] List memorias) + { + // Consulta para verificar la existencia. Maneja correctamente los valores nulos. + var queryCheck = @"SELECT * FROM dbo.memorias_ram WHERE + (part_number = @Part_number OR (part_number IS NULL AND @Part_number IS NULL)) AND + (fabricante = @Fabricante OR (fabricante IS NULL AND @Fabricante IS NULL)) AND + tamano = @Tamano AND + (velocidad = @Velocidad OR (velocidad IS NULL AND @Velocidad IS NULL));"; + + var queryInsert = @"INSERT INTO dbo.memorias_ram (part_number, fabricante, tamano, velocidad) + VALUES (@Part_number, @Fabricante, @Tamano, @Velocidad); + SELECT CAST(SCOPE_IDENTITY() as int);"; + + var resultados = new List(); + + using (var connection = _context.CreateConnection()) + { + foreach (var memoria in memorias) + { + var existente = await connection.QuerySingleOrDefaultAsync(queryCheck, memoria); + + if (existente == null) + { + var nuevoId = await connection.ExecuteScalarAsync(queryInsert, memoria); + var nuevaMemoria = new MemoriaRam + { + Id = nuevoId, + Part_number = memoria.Part_number, + Fabricante = memoria.Fabricante, + Tamano = memoria.Tamano, + Velocidad = memoria.Velocidad + }; + resultados.Add(new { action = "created", registro = nuevaMemoria }); + } + else + { + resultados.Add(new { action = "exists", registro = existente }); + } + } + } + return Ok(resultados); + } + + // --- PUT /api/memoriasram/{id} --- + [HttpPut("{id}")] + public async Task Actualizar(int id, [FromBody] MemoriaRam memoria) + { + var query = @"UPDATE dbo.memorias_ram SET + part_number = @Part_number, + fabricante = @Fabricante, + tamano = @Tamano, + velocidad = @Velocidad + WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + var filasAfectadas = await connection.ExecuteAsync(query, new { memoria.Part_number, memoria.Fabricante, memoria.Tamano, memoria.Velocidad, Id = id }); + if (filasAfectadas == 0) + { + return NotFound("Módulo de memoria RAM no encontrado."); + } + + memoria.Id = id; + return Ok(memoria); + } + } + + // --- DELETE /api/memoriasram/{id} --- + [HttpDelete("{id}")] + public async Task Borrar(int id) + { + var deleteAssociationsQuery = "DELETE FROM dbo.equipos_memorias_ram WHERE memoria_ram_id = @Id;"; + var deleteRamQuery = "DELETE FROM dbo.memorias_ram WHERE Id = @Id;"; + + using (var connection = _context.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + { + try + { + // Primero eliminamos las asociaciones en la tabla intermedia + await connection.ExecuteAsync(deleteAssociationsQuery, new { Id = id }, transaction: transaction); + + // Luego eliminamos el módulo de RAM + var filasAfectadas = await connection.ExecuteAsync(deleteRamQuery, new { Id = id }, transaction: transaction); + + if (filasAfectadas == 0) + { + // Si no se borró nada, hacemos rollback y devolvemos NotFound. + transaction.Rollback(); + return NotFound("Módulo de memoria RAM no encontrado."); + } + + // Si todo salió bien, confirmamos la transacción. + transaction.Commit(); + return NoContent(); + } + catch + { + transaction.Rollback(); + throw; // Relanza la excepción para que sea manejada por el middleware de errores de ASP.NET Core + } + } + } + } + } +} \ No newline at end of file diff --git a/backend/Controllers/SectoresController.cs b/backend/Controllers/SectoresController.cs index 7858464..291053a 100644 --- a/backend/Controllers/SectoresController.cs +++ b/backend/Controllers/SectoresController.cs @@ -5,40 +5,118 @@ using Microsoft.AspNetCore.Mvc; namespace Inventario.API.Controllers { - // [ApiController] habilita comportamientos estándar de API como la validación automática. [ApiController] - // [Route("api/[controller]")] define la URL base para este controlador. - // "[controller]" se reemplaza por el nombre de la clase sin "Controller", es decir, "api/sectores". [Route("api/[controller]")] public class SectoresController : ControllerBase { - // Campo privado para guardar la referencia a nuestro contexto de Dapper. private readonly DapperContext _context; - // El constructor recibe el DapperContext a través de la inyección de dependencias que configuramos en Program.cs. public SectoresController(DapperContext context) { _context = context; } - // [HttpGet] marca este método para que responda a peticiones GET a la ruta base ("api/sectores"). + // --- GET /api/sectores --- + // Método para obtener todos los sectores. [HttpGet] public async Task ConsultarSectores() { - // Definimos nuestra consulta SQL. Es buena práctica listar las columnas explícitamente. var query = "SELECT Id, Nombre FROM dbo.sectores ORDER BY Nombre;"; - - // Creamos una conexión a la base de datos. El 'using' asegura que la conexión se cierre y se libere correctamente, incluso si hay errores. using (var connection = _context.CreateConnection()) { - // Usamos el método QueryAsync de Dapper. - // le dice a Dapper que mapee cada fila del resultado a un objeto de nuestra clase Sector. - // 'await' espera a que la base de datos responda sin bloquear el servidor. var sectores = await connection.QueryAsync(query); - - // Ok() crea una respuesta HTTP 200 OK y serializa la lista de sectores a formato JSON. return Ok(sectores); } } + + // --- GET /api/sectores/{id} --- + // Método para obtener un único sector por su ID. + [HttpGet("{id}")] + public async Task ConsultarSectorDetalle(int id) + { + var query = "SELECT Id, Nombre FROM dbo.sectores WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + // QuerySingleOrDefaultAsync es perfecto para obtener un solo registro. + // Devuelve el objeto si lo encuentra, o null si no existe. + var sector = await connection.QuerySingleOrDefaultAsync(query, new { Id = id }); + + if (sector == null) + { + // Si no se encuentra, devolvemos un error HTTP 404 Not Found. + return NotFound(); + } + return Ok(sector); + } + } + + // --- POST /api/sectores --- + // Método para crear un nuevo sector. + // Nota: Cambiado de /:nombre a un POST estándar que recibe el objeto en el body. Es más RESTful. + [HttpPost] + public async Task IngresarSector([FromBody] Sector sector) + { + var checkQuery = "SELECT COUNT(1) FROM dbo.sectores WHERE Nombre = @Nombre;"; + var insertQuery = "INSERT INTO dbo.sectores (Nombre) VALUES (@Nombre); SELECT CAST(SCOPE_IDENTITY() as int);"; + + using (var connection = _context.CreateConnection()) + { + // Primero, verificamos si ya existe un sector con ese nombre. + var existe = await connection.ExecuteScalarAsync(checkQuery, new { sector.Nombre }); + if (existe) + { + // Si ya existe, devolvemos un error HTTP 409 Conflict. + return Conflict($"Ya existe un sector con el nombre '{sector.Nombre}'"); + } + + // ExecuteScalarAsync ejecuta la consulta y devuelve el primer valor de la primera fila (el nuevo ID). + var nuevoId = await connection.ExecuteScalarAsync(insertQuery, new { sector.Nombre }); + var nuevoSector = new Sector { Id = nuevoId, Nombre = sector.Nombre }; + + // Devolvemos una respuesta HTTP 201 Created, con la ubicación del nuevo recurso y el objeto creado. + return CreatedAtAction(nameof(ConsultarSectorDetalle), new { id = nuevoId }, nuevoSector); + } + } + + // --- PUT /api/sectores/{id} --- + // Método para actualizar un sector existente. + [HttpPut("{id}")] + public async Task ActualizarSector(int id, [FromBody] Sector sector) + { + var query = "UPDATE dbo.sectores SET Nombre = @Nombre WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + // ExecuteAsync devuelve el número de filas afectadas. + var filasAfectadas = await connection.ExecuteAsync(query, new { Nombre = sector.Nombre, Id = id }); + + if (filasAfectadas == 0) + { + // Si no se afectó ninguna fila, significa que el ID no existía. + return NotFound(); + } + + // Devolvemos HTTP 204 No Content para indicar que la actualización fue exitosa pero no hay nada que devolver. + return NoContent(); + } + } + + // --- DELETE /api/sectores/{id} --- + // Método para eliminar un sector. + [HttpDelete("{id}")] + public async Task BorrarSector(int id) + { + var query = "DELETE FROM dbo.sectores WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + var filasAfectadas = await connection.ExecuteAsync(query, new { Id = id }); + + if (filasAfectadas == 0) + { + return NotFound(); + } + + return NoContent(); + } + } } } \ No newline at end of file diff --git a/backend/Controllers/UsuariosController.cs b/backend/Controllers/UsuariosController.cs new file mode 100644 index 0000000..e722bf5 --- /dev/null +++ b/backend/Controllers/UsuariosController.cs @@ -0,0 +1,123 @@ +using Dapper; +using Inventario.API.Data; +using Inventario.API.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Inventario.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class UsuariosController : ControllerBase + { + private readonly DapperContext _context; + + public UsuariosController(DapperContext context) + { + _context = context; + } + + // --- GET /api/usuarios --- + [HttpGet] + public async Task Consultar() + { + var query = "SELECT Id, Username, Password, Created_at, Updated_at FROM dbo.usuarios ORDER BY Username;"; + using (var connection = _context.CreateConnection()) + { + var usuarios = await connection.QueryAsync(query); + return Ok(usuarios); + } + } + + // --- GET /api/usuarios/{id} --- + [HttpGet("{id}")] + public async Task ConsultarDetalle(int id) + { + var query = "SELECT Id, Username, Password, Created_at, Updated_at FROM dbo.usuarios WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + var usuario = await connection.QuerySingleOrDefaultAsync(query, new { Id = id }); + if (usuario == null) + { + return NotFound("Usuario no encontrado."); + } + return Ok(usuario); + } + } + + // --- POST /api/usuarios --- + // Este endpoint replica la lógica "upsert" del original: si el usuario existe, lo actualiza; si no, lo crea. + [HttpPost] + public async Task Ingresar([FromBody] Usuario usuario) + { + var findQuery = "SELECT * FROM dbo.usuarios WHERE Username = @Username;"; + using (var connection = _context.CreateConnection()) + { + var usuarioExistente = await connection.QuerySingleOrDefaultAsync(findQuery, new { usuario.Username }); + + if (usuarioExistente != null) + { + // El usuario ya existe, lo actualizamos (solo la contraseña si viene) + var updateQuery = "UPDATE dbo.usuarios SET Password = @Password WHERE Id = @Id;"; + await connection.ExecuteAsync(updateQuery, new { usuario.Password, Id = usuarioExistente.Id }); + + // Devolvemos el usuario actualizado + var usuarioActualizado = await connection.QuerySingleOrDefaultAsync(findQuery, new { usuario.Username }); + return Ok(usuarioActualizado); + } + else + { + // El usuario no existe, lo creamos + var insertQuery = "INSERT INTO dbo.usuarios (Username, Password) VALUES (@Username, @Password); SELECT CAST(SCOPE_IDENTITY() as int);"; + var nuevoId = await connection.ExecuteScalarAsync(insertQuery, new { usuario.Username, usuario.Password }); + + var nuevoUsuario = new Usuario + { + Id = nuevoId, + Username = usuario.Username, + Password = usuario.Password + }; + return CreatedAtAction(nameof(ConsultarDetalle), new { id = nuevoId }, nuevoUsuario); + } + } + } + + // --- PUT /api/usuarios/{id} --- + // Endpoint específico para actualizar la contraseña, como en el original. + [HttpPut("{id}")] + public async Task Actualizar(int id, [FromBody] Usuario data) + { + var updateQuery = "UPDATE dbo.usuarios SET Password = @Password WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + var filasAfectadas = await connection.ExecuteAsync(updateQuery, new { data.Password, Id = id }); + + if (filasAfectadas == 0) + { + return NotFound("Usuario no encontrado."); + } + + // Para replicar la respuesta del backend original, volvemos a consultar el usuario (sin la contraseña). + var selectQuery = "SELECT Id, Username FROM dbo.usuarios WHERE Id = @Id;"; + var usuarioActualizado = await connection.QuerySingleOrDefaultAsync(selectQuery, new { Id = id }); + + return Ok(usuarioActualizado); + } + } + + // --- DELETE /api/usuarios/{id} --- + [HttpDelete("{id}")] + public async Task Borrar(int id) + { + var query = "DELETE FROM dbo.usuarios WHERE Id = @Id;"; + using (var connection = _context.CreateConnection()) + { + var filasAfectadas = await connection.ExecuteAsync(query, new { Id = id }); + if (filasAfectadas == 0) + { + return NotFound("Usuario no encontrado."); + } + return NoContent(); // Respuesta HTTP 204 No Content + } + } + } +} \ No newline at end of file diff --git a/backend/Helpers/HistorialHelper.cs b/backend/Helpers/HistorialHelper.cs new file mode 100644 index 0000000..d410654 --- /dev/null +++ b/backend/Helpers/HistorialHelper.cs @@ -0,0 +1,29 @@ +using Dapper; +using Inventario.API.Data; +using Inventario.API.Models; + +namespace Inventario.API.Helpers +{ + public static class HistorialHelper + { + public static async Task RegistrarCambios(DapperContext context, int equipoId, Dictionary cambios) + { + var query = @"INSERT INTO dbo.historial_equipos (equipo_id, campo_modificado, valor_anterior, valor_nuevo) + VALUES (@EquipoId, @CampoModificado, @ValorAnterior, @ValorNuevo);"; + + using (var connection = context.CreateConnection()) + { + foreach (var cambio in cambios) + { + await connection.ExecuteAsync(query, new + { + EquipoId = equipoId, + CampoModificado = cambio.Key, + ValorAnterior = cambio.Value.anterior, + ValorNuevo = cambio.Value.nuevo + }); + } + } + } + } +} \ No newline at end of file diff --git a/backend/obj/Debug/net9.0/Inventario.API.AssemblyInfo.cs b/backend/obj/Debug/net9.0/Inventario.API.AssemblyInfo.cs index ae9f5c7..c8eda31 100644 --- a/backend/obj/Debug/net9.0/Inventario.API.AssemblyInfo.cs +++ b/backend/obj/Debug/net9.0/Inventario.API.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Inventario.API")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+80eeac45d8b90ed69a6479e9d3dcadde5e317a90")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+10f2f2ba67eac900e8a7b63ced2f3689c309fa6f")] [assembly: System.Reflection.AssemblyProductAttribute("Inventario.API")] [assembly: System.Reflection.AssemblyTitleAttribute("Inventario.API")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]