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