From fae9101c6391b86d9821b3b819344e2de2f73473 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 17 Dec 2025 13:21:50 -0300 Subject: [PATCH] =?UTF-8?q?Fase=202:=20Implementaci=C3=B3n=20de=20la=20Mat?= =?UTF-8?q?riz=20de=20Operaciones=20(Backend=20&=20Frontend)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/Categories/CategoryManager.tsx | 86 ++++++++++++++++++- .../src/pages/Categories/OperationManager.tsx | 71 +++++++++++++++ .../src/services/categoryService.ts | 13 +++ .../src/services/operationService.ts | 18 ++++ frontend/admin-panel/src/types/Operation.ts | 4 + .../Controllers/CategoriesController.cs | 22 +++++ .../Interfaces/ICategoryRepository.cs | 3 + .../Repositories/CategoryRepository.cs | 35 ++++++++ 8 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 frontend/admin-panel/src/pages/Categories/OperationManager.tsx create mode 100644 frontend/admin-panel/src/services/operationService.ts create mode 100644 frontend/admin-panel/src/types/Operation.ts diff --git a/frontend/admin-panel/src/pages/Categories/CategoryManager.tsx b/frontend/admin-panel/src/pages/Categories/CategoryManager.tsx index 4962f0c..1fd1215 100644 --- a/frontend/admin-panel/src/pages/Categories/CategoryManager.tsx +++ b/frontend/admin-panel/src/pages/Categories/CategoryManager.tsx @@ -1,9 +1,12 @@ import { useState, useEffect } from 'react'; import { Category } from '../../types/Category'; +import OperationManager from './OperationManager'; // Import added import { categoryService } from '../../services/categoryService'; import Modal from '../../components/Modal'; -import { Plus, Edit, Trash2, ChevronRight, ChevronDown, Folder, FolderOpen } from 'lucide-react'; +import { Plus, Edit, Trash2, ChevronRight, ChevronDown, Folder, FolderOpen, Settings } from 'lucide-react'; import clsx from 'clsx'; +import { operationService } from '../../services/operationService'; +import { Operation } from '../../types/Operation'; // Type helper for the tree structure interface CategoryNode extends Category { @@ -20,6 +23,12 @@ export default function CategoryManager() { const [editingCategory, setEditingCategory] = useState(null); const [formData, setFormData] = useState>({ name: '', slug: '', active: true, parentId: null }); + // New state for operations matrix + const [selectedCategoryOps, setSelectedCategoryOps] = useState([]); + const [allOperations, setAllOperations] = useState([]); + const [configuringCategory, setConfiguringCategory] = useState(null); + const [isOpsModalOpen, setIsOpsModalOpen] = useState(false); + useEffect(() => { loadCategories(); }, []); @@ -85,6 +94,39 @@ export default function CategoryManager() { } }; + const handleConfigureOps = async (category: Category) => { + setConfiguringCategory(category); + setIsOpsModalOpen(true); + + // Load data in parallel + const [ops, catOps] = await Promise.all([ + operationService.getAll(), + categoryService.getOperations(category.id) + ]); + + setAllOperations(ops); + setSelectedCategoryOps(catOps); + }; + + const toggleOperation = async (opId: number, isChecked: boolean) => { + if (!configuringCategory) return; + + try { + if (isChecked) { + await categoryService.addOperation(configuringCategory.id, opId); + // Optimistic update + const opToAdd = allOperations.find(o => o.id === opId); + if (opToAdd) setSelectedCategoryOps([...selectedCategoryOps, opToAdd]); + } else { + await categoryService.removeOperation(configuringCategory.id, opId); + setSelectedCategoryOps(selectedCategoryOps.filter(o => o.id !== opId)); + } + } catch (error) { + console.error(error); + alert("Error actualizando operación"); + } + }; + // Recursive component for rendering specific tree items const CategoryItem = ({ node }: { node: CategoryNode }) => { const [isExpanded, setIsExpanded] = useState(true); @@ -112,6 +154,13 @@ export default function CategoryManager() { {node.name}
+
+ {/* Operation Manager Section */} + +
{isLoading ? (
Cargando taxonomía...
@@ -228,6 +280,38 @@ export default function CategoryManager() {
+ + {/* Matrix Operations Modal */} + setIsOpsModalOpen(false)} + title={`Operaciones para: ${configuringCategory?.name}`} + > +
+

Seleccione las operaciones habilitadas para este rubro.

+ {allOperations.length === 0 &&

No hay operaciones definidas.

} +
+ {allOperations.map(op => { + const isSelected = selectedCategoryOps.some(so => so.id === op.id); + return ( + + ); + })} +
+
+ +
+
+
); + } diff --git a/frontend/admin-panel/src/pages/Categories/OperationManager.tsx b/frontend/admin-panel/src/pages/Categories/OperationManager.tsx new file mode 100644 index 0000000..6aec9c4 --- /dev/null +++ b/frontend/admin-panel/src/pages/Categories/OperationManager.tsx @@ -0,0 +1,71 @@ +import { useState, useEffect } from 'react'; +import { Operation } from '../../types/Operation'; // ID: type-import-fix +import { operationService } from '../../services/operationService'; +import { Plus, Trash2 } from 'lucide-react'; + +export default function OperationManager() { + const [operations, setOperations] = useState([]); + const [newOpName, setNewOpName] = useState(''); + + useEffect(() => { + loadOperations(); + }, []); + + const loadOperations = async () => { + const ops = await operationService.getAll(); + setOperations(ops); + }; + + const handleCreate = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newOpName.trim()) return; + + try { + await operationService.create(newOpName); + setNewOpName(''); + loadOperations(); + } catch (error) { + console.error(error); + } + }; + + const handleDelete = async (id: number) => { + if (!confirm('Eliminar operación?')) return; + try { + await operationService.delete(id); + loadOperations(); + } catch (error) { + console.error(error); + } + }; + + return ( +
+

Operaciones Globales

+ +
+ setNewOpName(e.target.value)} + placeholder="Nueva operación (ej: Venta)" + className="border p-2 rounded flex-1" + /> + +
+ +
+ {operations.map(op => ( +
+ {op.name} + +
+ ))} +
+
+ ); +} diff --git a/frontend/admin-panel/src/services/categoryService.ts b/frontend/admin-panel/src/services/categoryService.ts index f32baaa..4ce8782 100644 --- a/frontend/admin-panel/src/services/categoryService.ts +++ b/frontend/admin-panel/src/services/categoryService.ts @@ -23,5 +23,18 @@ export const categoryService = { delete: async (id: number): Promise => { await api.delete(`/categories/${id}`); + }, + + getOperations: async (id: number): Promise => { + const response = await api.get(`/categories/${id}/operations`); + return response.data; + }, + + addOperation: async (categoryId: number, operationId: number): Promise => { + await api.post(`/categories/${categoryId}/operations/${operationId}`); + }, + + removeOperation: async (categoryId: number, operationId: number): Promise => { + await api.delete(`/categories/${categoryId}/operations/${operationId}`); } }; diff --git a/frontend/admin-panel/src/services/operationService.ts b/frontend/admin-panel/src/services/operationService.ts new file mode 100644 index 0000000..48101e2 --- /dev/null +++ b/frontend/admin-panel/src/services/operationService.ts @@ -0,0 +1,18 @@ +import api from './api'; +import { Operation } from '../types/Operation'; // ID: type-import-fix + +export const operationService = { + getAll: async (): Promise => { + const response = await api.get('/operations'); + return response.data; + }, + + create: async (name: string): Promise => { + const response = await api.post('/operations', { name }); + return response.data; + }, + + delete: async (id: number): Promise => { + await api.delete(`/operations/${id}`); + } +}; diff --git a/frontend/admin-panel/src/types/Operation.ts b/frontend/admin-panel/src/types/Operation.ts new file mode 100644 index 0000000..e831457 --- /dev/null +++ b/frontend/admin-panel/src/types/Operation.ts @@ -0,0 +1,4 @@ +export interface Operation { + id: number; + name: string; +} diff --git a/src/SIGCM.API/Controllers/CategoriesController.cs b/src/SIGCM.API/Controllers/CategoriesController.cs index 7517f31..e96ac19 100644 --- a/src/SIGCM.API/Controllers/CategoriesController.cs +++ b/src/SIGCM.API/Controllers/CategoriesController.cs @@ -52,4 +52,26 @@ public class CategoriesController : ControllerBase await _repository.DeleteAsync(id); return NoContent(); } + + [HttpGet("{id}/operations")] + public async Task GetOperations(int id) + { + var operations = await _repository.GetOperationsAsync(id); + return Ok(operations); + } + + [HttpPost("{id}/operations/{operationId}")] + public async Task AddOperation(int id, int operationId) + { + await _repository.AddOperationAsync(id, operationId); + return Ok(); + } + + [HttpDelete("{id}/operations/{operationId}")] + public async Task RemoveOperation(int id, int operationId) + { + await _repository.RemoveOperationAsync(id, operationId); + return NoContent(); + } } + diff --git a/src/SIGCM.Domain/Interfaces/ICategoryRepository.cs b/src/SIGCM.Domain/Interfaces/ICategoryRepository.cs index 2e32457..e59c569 100644 --- a/src/SIGCM.Domain/Interfaces/ICategoryRepository.cs +++ b/src/SIGCM.Domain/Interfaces/ICategoryRepository.cs @@ -9,4 +9,7 @@ public interface ICategoryRepository Task UpdateAsync(Category category); Task DeleteAsync(int id); Task> GetSubCategoriesAsync(int parentId); + Task> GetOperationsAsync(int categoryId); + Task AddOperationAsync(int categoryId, int operationId); + Task RemoveOperationAsync(int categoryId, int operationId); } diff --git a/src/SIGCM.Infrastructure/Repositories/CategoryRepository.cs b/src/SIGCM.Infrastructure/Repositories/CategoryRepository.cs index f178ea6..6411628 100644 --- a/src/SIGCM.Infrastructure/Repositories/CategoryRepository.cs +++ b/src/SIGCM.Infrastructure/Repositories/CategoryRepository.cs @@ -1,4 +1,5 @@ using Dapper; +using Microsoft.Data.SqlClient; using SIGCM.Domain.Entities; using SIGCM.Domain.Interfaces; using SIGCM.Infrastructure.Data; @@ -57,4 +58,38 @@ public class CategoryRepository : ICategoryRepository using var conn = _connectionFactory.CreateConnection(); return await conn.QueryAsync("SELECT * FROM Categories WHERE ParentId = @ParentId", new { ParentId = parentId }); } + + public async Task> GetOperationsAsync(int categoryId) + { + using var conn = _connectionFactory.CreateConnection(); + var sql = @" + SELECT o.* + FROM Operations o + INNER JOIN CategoryOperations co ON o.Id = co.OperationId + WHERE co.CategoryId = @CategoryId"; + return await conn.QueryAsync(sql, new { CategoryId = categoryId }); + } + + public async Task AddOperationAsync(int categoryId, int operationId) + { + using var conn = _connectionFactory.CreateConnection(); + var sql = "INSERT INTO CategoryOperations (CategoryId, OperationId) VALUES (@CategoryId, @OperationId)"; + try + { + await conn.ExecuteAsync(sql, new { CategoryId = categoryId, OperationId = operationId }); + } + catch (SqlException) + { + // Ignore duplicate key errors if it already exists + } + } + + public async Task RemoveOperationAsync(int categoryId, int operationId) + { + using var conn = _connectionFactory.CreateConnection(); + await conn.ExecuteAsync( + "DELETE FROM CategoryOperations WHERE CategoryId = @CategoryId AND OperationId = @OperationId", + new { CategoryId = categoryId, OperationId = operationId }); + } } +