Fase 2: Gestión de Atributos Dinámicos (Back & Front EAV Definition)
This commit is contained in:
@@ -108,6 +108,43 @@ export default function CategoryManager() {
|
|||||||
setSelectedCategoryOps(catOps);
|
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) => {
|
const toggleOperation = async (opId: number, isChecked: boolean) => {
|
||||||
if (!configuringCategory) return;
|
if (!configuringCategory) return;
|
||||||
|
|
||||||
@@ -154,6 +191,15 @@ export default function CategoryManager() {
|
|||||||
<span className="flex-1">{node.name}</span>
|
<span className="flex-1">{node.name}</span>
|
||||||
|
|
||||||
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
<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
|
<button
|
||||||
onClick={() => handleConfigureOps(node)}
|
onClick={() => handleConfigureOps(node)}
|
||||||
className="p-1 text-purple-600 hover:bg-purple-100 rounded"
|
className="p-1 text-purple-600 hover:bg-purple-100 rounded"
|
||||||
|
|||||||
18
frontend/admin-panel/src/services/attributeService.ts
Normal file
18
frontend/admin-panel/src/services/attributeService.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
7
frontend/admin-panel/src/types/AttributeDefinition.ts
Normal file
7
frontend/admin-panel/src/types/AttributeDefinition.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface AttributeDefinition {
|
||||||
|
id: number;
|
||||||
|
categoryId: number;
|
||||||
|
name: string;
|
||||||
|
dataType: 'text' | 'number' | 'boolean' | 'date';
|
||||||
|
required: boolean;
|
||||||
|
}
|
||||||
39
src/SIGCM.API/Controllers/AttributeDefinitionsController.cs
Normal file
39
src/SIGCM.API/Controllers/AttributeDefinitionsController.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/SIGCM.Domain/Entities/AttributeDefinition.cs
Normal file
10
src/SIGCM.Domain/Entities/AttributeDefinition.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -111,6 +111,18 @@ BEGIN
|
|||||||
FOREIGN KEY (OperationId) REFERENCES Operations(Id) ON DELETE CASCADE
|
FOREIGN KEY (OperationId) REFERENCES Operations(Id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
END
|
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);
|
await connection.ExecuteAsync(schemaSql);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IUserRepository, UserRepository>();
|
services.AddScoped<IUserRepository, UserRepository>();
|
||||||
services.AddScoped<ITokenService, Services.TokenService>();
|
services.AddScoped<ITokenService, Services.TokenService>();
|
||||||
services.AddScoped<IAuthService, Services.AuthService>();
|
services.AddScoped<IAuthService, Services.AuthService>();
|
||||||
|
services.AddScoped<IAttributeDefinitionRepository, AttributeDefinitionRepository>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user