feat: Implementación de eliminación de productos con validación de asociaciones

This commit is contained in:
2026-02-21 20:41:41 -03:00
parent b8f1ed8a68
commit f3638195a6
5 changed files with 96 additions and 9 deletions

View File

@@ -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() {
<span className={clsx("w-2 h-2 rounded-full inline-block", p.isActive ? "bg-emerald-500" : "bg-rose-500")}></span>
</td>
<td className="p-5 text-right">
<button
onClick={() => handleEdit(p)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
<Edit size={18} />
</button>
<div className="flex justify-end gap-1">
<button
onClick={() => handleEdit(p)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Editar"
>
<Edit size={18} />
</button>
<button
onClick={() => handleDelete(p)}
className="p-2 text-rose-600 hover:bg-rose-50 rounded-lg transition-colors"
title="Eliminar"
>
<Trash2 size={18} />
</button>
</div>
</td>
</tr>
))}

View File

@@ -27,6 +27,10 @@ export const productService = {
await api.put(`/products/${id}`, product);
},
delete: async (id: number): Promise<void> => {
await api.delete(`/products/${id}`);
},
// --- GESTIÓN DE COMBOS (BUNDLES) ---
/**

View File

@@ -70,6 +70,25 @@ public class ProductsController : ControllerBase
return NoContent();
}
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> 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<IActionResult> GetCompanies()

View File

@@ -16,4 +16,5 @@ public interface IProductRepository
Task RemoveComponentFromBundleAsync(int bundleId, int childProductId);
Task<decimal> GetCurrentPriceAsync(int productId, DateTime date);
Task AddPriceAsync(ProductPrice price);
Task DeleteAsync(int id);
}

View File

@@ -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<int>(
"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<int>(@"
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 });
}
}