From f3638195a6d12ff07aedc7ff1ac27c920cd3ef39 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 21 Feb 2026 20:41:41 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Implementaci=C3=B3n=20de=20eliminaci?= =?UTF-8?q?=C3=B3n=20de=20productos=20con=20validaci=C3=B3n=20de=20asociac?= =?UTF-8?q?iones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/Products/ProductManager.tsx | 39 +++++++++++++---- .../src/services/productService.ts | 4 ++ .../Controllers/ProductsController.cs | 19 +++++++++ .../Interfaces/IProductRepository.cs | 1 + .../Repositories/ProductRepository.cs | 42 ++++++++++++++++++- 5 files changed, 96 insertions(+), 9 deletions(-) diff --git a/frontend/admin-panel/src/pages/Products/ProductManager.tsx b/frontend/admin-panel/src/pages/Products/ProductManager.tsx index 7de7cb0..42bfaef 100644 --- a/frontend/admin-panel/src/pages/Products/ProductManager.tsx +++ b/frontend/admin-panel/src/pages/Products/ProductManager.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Plus, Search, Edit, Box, Layers } from 'lucide-react'; +import { Plus, Search, Edit, Box, Layers, Trash2 } from 'lucide-react'; import { productService } from '../../../../counter-panel/src/services/productService'; import { companyService } from '../../../../counter-panel/src/services/companyService'; import { categoryService } from '../../services/categoryService'; @@ -57,6 +57,21 @@ export default function ProductManager() { if (shouldRefresh) loadData(); }; + const handleDelete = async (product: Product) => { + if (!window.confirm(`¿Está seguro que desea eliminar el producto "${product.name}"? Esta acción no se puede deshacer.`)) { + return; + } + + try { + await productService.delete(product.id); + loadData(); + } catch (error: any) { + console.error("Error eliminando producto", error); + const msg = error.response?.data?.message || "Ocurrió un error al intentar eliminar el producto."; + alert(msg); + } + }; + const filteredProducts = products.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()) || p.sku?.toLowerCase().includes(searchTerm.toLowerCase()) @@ -138,12 +153,22 @@ export default function ProductManager() { - +
+ + +
))} diff --git a/frontend/counter-panel/src/services/productService.ts b/frontend/counter-panel/src/services/productService.ts index d9a74da..a00ab3d 100644 --- a/frontend/counter-panel/src/services/productService.ts +++ b/frontend/counter-panel/src/services/productService.ts @@ -27,6 +27,10 @@ export const productService = { await api.put(`/products/${id}`, product); }, + delete: async (id: number): Promise => { + await api.delete(`/products/${id}`); + }, + // --- GESTIÓN DE COMBOS (BUNDLES) --- /** diff --git a/src/SIGCM.API/Controllers/ProductsController.cs b/src/SIGCM.API/Controllers/ProductsController.cs index af9dbc7..14ee44a 100644 --- a/src/SIGCM.API/Controllers/ProductsController.cs +++ b/src/SIGCM.API/Controllers/ProductsController.cs @@ -70,6 +70,25 @@ public class ProductsController : ControllerBase return NoContent(); } + [HttpDelete("{id}")] + [Authorize(Roles = "Admin")] + public async Task Delete(int id) + { + try + { + await _repository.DeleteAsync(id); + return NoContent(); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { message = ex.Message }); + } + catch (Exception) + { + return StatusCode(500, new { message = "Error inesperado al intentar eliminar el producto." }); + } + } + // Helper: Obtener lista de empresas para llenar el combo en el Frontend [HttpGet("companies")] public async Task GetCompanies() diff --git a/src/SIGCM.Domain/Interfaces/IProductRepository.cs b/src/SIGCM.Domain/Interfaces/IProductRepository.cs index afdc014..83b9495 100644 --- a/src/SIGCM.Domain/Interfaces/IProductRepository.cs +++ b/src/SIGCM.Domain/Interfaces/IProductRepository.cs @@ -16,4 +16,5 @@ public interface IProductRepository Task RemoveComponentFromBundleAsync(int bundleId, int childProductId); Task GetCurrentPriceAsync(int productId, DateTime date); Task AddPriceAsync(ProductPrice price); + Task DeleteAsync(int id); } \ No newline at end of file diff --git a/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs b/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs index e2a4c77..2c8d131 100644 --- a/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs +++ b/src/SIGCM.Infrastructure/Repositories/ProductRepository.cs @@ -149,8 +149,8 @@ public class ProductRepository : IProductRepository { using var conn = _db.CreateConnection(); await conn.ExecuteAsync( - "DELETE FROM ProductBundles WHERE ParentProductId = @ParentId AND ChildProductId = @ChildId", - new { ParentId = bundleId, ChildId = childProductId }); + "DELETE FROM ProductBundles WHERE ParentProductId = @ParentId AND ChildProductId = @Id", + new { ParentId = bundleId, Id = childProductId }); } // Obtener el precio vigente para una fecha dada @@ -187,4 +187,42 @@ public class ProductRepository : IProductRepository VALUES (@ProductId, @Price, @ValidFrom, @ValidTo, @CreatedByUserId)"; await conn.ExecuteAsync(sql, price); } + + public async Task DeleteAsync(int id) + { + using var conn = _db.CreateConnection(); + // 1. Verificar si está siendo usado como componente de un combo + var usedInBundle = await conn.ExecuteScalarAsync( + "SELECT COUNT(1) FROM ProductBundles WHERE ChildProductId = @Id", new { Id = id }); + + if (usedInBundle > 0) + { + throw new InvalidOperationException("No se puede eliminar el producto porque forma parte de uno o más combos."); + } + + // 2. Verificar si tiene registros asociados (ej: Listings) + // Buscamos dinámicamente si existe la columna ProductId en Listings o si hay alguna tabla que lo use + var hasSales = await conn.ExecuteScalarAsync(@" + IF EXISTS (SELECT 1 FROM sys.columns WHERE Name = 'ProductId' AND Object_ID = Object_ID('Listings')) + BEGIN + SELECT COUNT(1) FROM Listings WHERE ProductId = @Id + END + ELSE + BEGIN + SELECT 0 + END", new { Id = id }); + + if (hasSales > 0) + { + throw new InvalidOperationException("No se puede eliminar el producto porque ya tiene ventas o registros asociados."); + } + + // 3. Eliminar registros permitidos + // Borramos precios históricos y relaciones de bundle (si es el padre) + await conn.ExecuteAsync("DELETE FROM ProductPrices WHERE ProductId = @Id", new { Id = id }); + await conn.ExecuteAsync("DELETE FROM ProductBundles WHERE ParentProductId = @Id", new { Id = id }); + + // 4. Eliminar el producto + await conn.ExecuteAsync("DELETE FROM Products WHERE Id = @Id", new { Id = id }); + } } \ No newline at end of file