Fase 2: Gestión de Atributos Dinámicos (Back & Front EAV Definition)

This commit is contained in:
2025-12-17 13:25:35 -03:00
parent fae9101c63
commit b3b553495b
9 changed files with 184 additions and 0 deletions

View File

@@ -108,6 +108,43 @@ export default function CategoryManager() {
setSelectedCategoryOps(catOps);
};
const handleConfigureAttributes = async (category: Category) => {
setConfiguringCategory(category);
setIsAttrModalOpen(true);
loadAttributes(category.id);
};
const loadAttributes = async (categoryId: number) => {
const attrs = await attributeService.getByCategoryId(categoryId);
setAttributes(attrs);
};
const handleCreateAttribute = async (e: React.FormEvent) => {
e.preventDefault();
if (!configuringCategory || !newAttrData.name) return;
try {
await attributeService.create({
...newAttrData,
categoryId: configuringCategory.id
});
setNewAttrData({ name: '', dataType: 'text', required: false });
loadAttributes(configuringCategory.id);
} catch (error) {
console.error(error);
}
};
const handleDeleteAttribute = async (id: number) => {
if (!confirm('Eliminar atributo?')) return;
try {
await attributeService.delete(id);
if (configuringCategory) loadAttributes(configuringCategory.id);
} catch (error) {
console.error(error);
}
};
const toggleOperation = async (opId: number, isChecked: boolean) => {
if (!configuringCategory) return;
@@ -154,6 +191,15 @@ export default function CategoryManager() {
<span className="flex-1">{node.name}</span>
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
{/* Attributes Button */}
<button
onClick={() => handleConfigureAttributes(node)}
className="p-1 text-orange-600 hover:bg-orange-100 rounded"
title="Atributos Dinámicos"
>
<LayoutList size={16} />
</button>
<button
onClick={() => handleConfigureOps(node)}
className="p-1 text-purple-600 hover:bg-purple-100 rounded"

View File

@@ -0,0 +1,18 @@
import api from './api';
import { AttributeDefinition } from '../types/AttributeDefinition';
export const attributeService = {
getByCategoryId: async (categoryId: number): Promise<AttributeDefinition[]> => {
const response = await api.get<AttributeDefinition[]>(`/attributedefinitions/category/${categoryId}`);
return response.data;
},
create: async (attribute: Partial<AttributeDefinition>): Promise<AttributeDefinition> => {
const response = await api.post<AttributeDefinition>('/attributedefinitions', attribute);
return response.data;
},
delete: async (id: number): Promise<void> => {
await api.delete(`/attributedefinitions/${id}`);
}
};

View File

@@ -0,0 +1,7 @@
export interface AttributeDefinition {
id: number;
categoryId: number;
name: string;
dataType: 'text' | 'number' | 'boolean' | 'date';
required: boolean;
}

View File

@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Mvc;
using SIGCM.Domain.Entities;
using SIGCM.Domain.Interfaces;
namespace SIGCM.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AttributeDefinitionsController : ControllerBase
{
private readonly IAttributeDefinitionRepository _repository;
public AttributeDefinitionsController(IAttributeDefinitionRepository repository)
{
_repository = repository;
}
[HttpGet("category/{categoryId}")]
public async Task<IActionResult> GetByCategoryId(int categoryId)
{
var attributes = await _repository.GetByCategoryIdAsync(categoryId);
return Ok(attributes);
}
[HttpPost]
public async Task<IActionResult> Create(AttributeDefinition attribute)
{
var id = await _repository.AddAsync(attribute);
attribute.Id = id;
return Ok(attribute);
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
await _repository.DeleteAsync(id);
return NoContent();
}
}

View File

@@ -0,0 +1,10 @@
namespace SIGCM.Domain.Entities;
public class AttributeDefinition
{
public int Id { get; set; }
public int CategoryId { get; set; }
public required string Name { get; set; }
public required string DataType { get; set; } // text, number, boolean, date
public bool Required { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace SIGCM.Domain.Interfaces;
using SIGCM.Domain.Entities;
public interface IAttributeDefinitionRepository
{
Task<IEnumerable<AttributeDefinition>> GetByCategoryIdAsync(int categoryId);
Task<int> AddAsync(AttributeDefinition attribute);
Task DeleteAsync(int id);
}

View File

@@ -111,6 +111,18 @@ BEGIN
FOREIGN KEY (OperationId) REFERENCES Operations(Id) ON DELETE CASCADE
);
END
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AttributeDefinitions')
BEGIN
CREATE TABLE AttributeDefinitions (
Id INT IDENTITY(1,1) PRIMARY KEY,
CategoryId INT NOT NULL,
Name NVARCHAR(100) NOT NULL,
DataType NVARCHAR(20) NOT NULL,
Required BIT DEFAULT 0,
FOREIGN KEY (CategoryId) REFERENCES Categories(Id) ON DELETE CASCADE
);
END
";
await connection.ExecuteAsync(schemaSql);
}

View File

@@ -17,9 +17,11 @@ public static class DependencyInjection
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<ITokenService, Services.TokenService>();
services.AddScoped<IAuthService, Services.AuthService>();
services.AddScoped<IAttributeDefinitionRepository, AttributeDefinitionRepository>();
return services;
}
}

View File

@@ -0,0 +1,41 @@
using Dapper;
using Microsoft.Data.SqlClient;
using SIGCM.Domain.Entities;
using SIGCM.Domain.Interfaces;
using SIGCM.Infrastructure.Data;
namespace SIGCM.Infrastructure.Repositories;
public class AttributeDefinitionRepository : IAttributeDefinitionRepository
{
private readonly IDbConnectionFactory _connectionFactory;
public AttributeDefinitionRepository(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<IEnumerable<AttributeDefinition>> GetByCategoryIdAsync(int categoryId)
{
using var conn = _connectionFactory.CreateConnection();
return await conn.QueryAsync<AttributeDefinition>(
"SELECT * FROM AttributeDefinitions WHERE CategoryId = @CategoryId",
new { CategoryId = categoryId });
}
public async Task<int> AddAsync(AttributeDefinition attribute)
{
using var conn = _connectionFactory.CreateConnection();
var sql = @"
INSERT INTO AttributeDefinitions (CategoryId, Name, DataType, Required)
VALUES (@CategoryId, @Name, @DataType, @Required);
SELECT CAST(SCOPE_IDENTITY() as int);";
return await conn.QuerySingleAsync<int>(sql, attribute);
}
public async Task DeleteAsync(int id)
{
using var conn = _connectionFactory.CreateConnection();
await conn.ExecuteAsync("DELETE FROM AttributeDefinitions WHERE Id = @Id", new { Id = id });
}
}