From 523a8394aef92cbd413059de5181d3b7d6fb8e85 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Wed, 17 Dec 2025 13:16:55 -0300 Subject: [PATCH] =?UTF-8?q?Fase=202:=20Gestor=20de=20Taxonom=C3=ADa=20(?= =?UTF-8?q?=C3=81rbol=20de=20Categor=C3=ADas)=20-=20Backend=20CORS=20y=20F?= =?UTF-8?q?rontend=20CRUD=20Recursivo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/admin-panel/src/App.tsx | 3 + frontend/admin-panel/src/components/Modal.tsx | 28 +++ .../src/layouts/ProtectedLayout.tsx | 3 + .../src/pages/Categories/CategoryManager.tsx | 233 ++++++++++++++++++ .../src/services/categoryService.ts | 27 ++ frontend/admin-panel/src/types/Category.ts | 8 + src/SIGCM.API/Program.cs | 13 + 7 files changed, 315 insertions(+) create mode 100644 frontend/admin-panel/src/components/Modal.tsx create mode 100644 frontend/admin-panel/src/pages/Categories/CategoryManager.tsx create mode 100644 frontend/admin-panel/src/services/categoryService.ts create mode 100644 frontend/admin-panel/src/types/Category.ts diff --git a/frontend/admin-panel/src/App.tsx b/frontend/admin-panel/src/App.tsx index ceb59a0..b5060a8 100644 --- a/frontend/admin-panel/src/App.tsx +++ b/frontend/admin-panel/src/App.tsx @@ -3,6 +3,8 @@ import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; import ProtectedLayout from './layouts/ProtectedLayout'; +import CategoryManager from './pages/Categories/CategoryManager'; + function App() { return ( @@ -11,6 +13,7 @@ function App() { }> } /> + } /> diff --git a/frontend/admin-panel/src/components/Modal.tsx b/frontend/admin-panel/src/components/Modal.tsx new file mode 100644 index 0000000..140c7cd --- /dev/null +++ b/frontend/admin-panel/src/components/Modal.tsx @@ -0,0 +1,28 @@ +import { X } from 'lucide-react'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + children: React.ReactNode; +} + +export default function Modal({ isOpen, onClose, title, children }: ModalProps) { + if (!isOpen) return null; + + return ( +
+
+
+

{title}

+ +
+
+ {children} +
+
+
+ ); +} diff --git a/frontend/admin-panel/src/layouts/ProtectedLayout.tsx b/frontend/admin-panel/src/layouts/ProtectedLayout.tsx index a87d2b9..82c8740 100644 --- a/frontend/admin-panel/src/layouts/ProtectedLayout.tsx +++ b/frontend/admin-panel/src/layouts/ProtectedLayout.tsx @@ -21,6 +21,9 @@ export default function ProtectedLayout() {
  • Dashboard
  • +
  • + Categorías +
  • diff --git a/frontend/admin-panel/src/pages/Categories/CategoryManager.tsx b/frontend/admin-panel/src/pages/Categories/CategoryManager.tsx new file mode 100644 index 0000000..4962f0c --- /dev/null +++ b/frontend/admin-panel/src/pages/Categories/CategoryManager.tsx @@ -0,0 +1,233 @@ +import { useState, useEffect } from 'react'; +import { Category } from '../../types/Category'; +import { categoryService } from '../../services/categoryService'; +import Modal from '../../components/Modal'; +import { Plus, Edit, Trash2, ChevronRight, ChevronDown, Folder, FolderOpen } from 'lucide-react'; +import clsx from 'clsx'; + +// Type helper for the tree structure +interface CategoryNode extends Category { + children: CategoryNode[]; + level: number; +} + +export default function CategoryManager() { + const [categories, setCategories] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + + // Form state + const [editingCategory, setEditingCategory] = useState(null); + const [formData, setFormData] = useState>({ name: '', slug: '', active: true, parentId: null }); + + useEffect(() => { + loadCategories(); + }, []); + + const loadCategories = async () => { + setIsLoading(true); + try { + const data = await categoryService.getAll(); + const tree = buildTree(data); + setCategories(tree); + } catch (error) { + console.error('Error loading categories:', error); + } finally { + setIsLoading(false); + } + }; + + const buildTree = (cats: Category[], parentId: number | null = null, level = 0): CategoryNode[] => { + return cats + .filter(c => c.parentId === parentId) + .map(c => ({ + ...c, + level, + children: buildTree(cats, c.id, level + 1) + })); + }; + + const handleCreate = (parentId: number | null = null) => { + setEditingCategory(null); + setFormData({ name: '', slug: '', active: true, parentId }); + setIsModalOpen(true); + }; + + const handleEdit = (category: Category) => { + setEditingCategory(category); + setFormData({ ...category }); + setIsModalOpen(true); + }; + + const handleDelete = async (id: number) => { + if (!confirm('¿Está seguro de eliminar esta categoría y todas sus subcategorías?')) return; + try { + await categoryService.delete(id); + loadCategories(); + } catch (error) { + console.error('Error deleting category:', error); + alert('Error al eliminar'); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (editingCategory) { + await categoryService.update(editingCategory.id, formData); + } else { + await categoryService.create(formData); + } + setIsModalOpen(false); + loadCategories(); + } catch (error) { + console.error('Error saving category:', error); + } + }; + + // Recursive component for rendering specific tree items + const CategoryItem = ({ node }: { node: CategoryNode }) => { + const [isExpanded, setIsExpanded] = useState(true); + + return ( +
    +
    + + + + {isExpanded ? : } + + + {node.name} + +
    + + + +
    +
    + + {isExpanded && node.children.length > 0 && ( +
    + {node.children.map(child => ( + + ))} +
    + )} +
    + ); + }; + + return ( +
    +
    +

    Estructura de Categorías

    + +
    + +
    + {isLoading ? ( +
    Cargando taxonomía...
    + ) : categories.length === 0 ? ( +
    No hay categorías definidas. Crea la primera.
    + ) : ( +
    + {categories.map(node => ( + + ))} +
    + )} +
    + + setIsModalOpen(false)} + title={editingCategory ? 'Editar Categoría' : 'Nueva Categoría'} + > +
    +
    + + setFormData({ ...formData, name: e.target.value })} + className="w-full border border-gray-300 rounded p-2 focus:ring-2 focus:ring-blue-500 outline-none" + /> +
    +
    + + setFormData({ ...formData, slug: e.target.value })} + className="w-full border border-gray-300 rounded p-2 focus:ring-2 focus:ring-blue-500 outline-none" + /> +
    +
    + setFormData({ ...formData, active: e.target.checked })} + id="activeCheck" + className="rounded text-blue-600 focus:ring-blue-500" + /> + +
    + +
    + + +
    +
    +
    +
    + ); +} diff --git a/frontend/admin-panel/src/services/categoryService.ts b/frontend/admin-panel/src/services/categoryService.ts new file mode 100644 index 0000000..f32baaa --- /dev/null +++ b/frontend/admin-panel/src/services/categoryService.ts @@ -0,0 +1,27 @@ +import api from './api'; +import { Category } from '../types/Category'; + +export const categoryService = { + getAll: async (): Promise => { + const response = await api.get('/categories'); + return response.data; + }, + + getById: async (id: number): Promise => { + const response = await api.get(`/categories/${id}`); + return response.data; + }, + + create: async (category: Partial): Promise => { + const response = await api.post('/categories', category); + return response.data; + }, + + update: async (id: number, category: Partial): Promise => { + await api.put(`/categories/${id}`, category); + }, + + delete: async (id: number): Promise => { + await api.delete(`/categories/${id}`); + } +}; diff --git a/frontend/admin-panel/src/types/Category.ts b/frontend/admin-panel/src/types/Category.ts new file mode 100644 index 0000000..e934c81 --- /dev/null +++ b/frontend/admin-panel/src/types/Category.ts @@ -0,0 +1,8 @@ +export interface Category { + id: number; + parentId?: number | null; + name: string; + slug: string; + active: boolean; + subcategories?: Category[]; // Para uso en el frontend +} diff --git a/src/SIGCM.API/Program.cs b/src/SIGCM.API/Program.cs index 2c9d849..18d7148 100644 --- a/src/SIGCM.API/Program.cs +++ b/src/SIGCM.API/Program.cs @@ -10,6 +10,17 @@ builder.Services.AddSwaggerGen(); builder.Services.AddInfrastructure(); +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowFrontend", + policy => + { + policy.WithOrigins("http://localhost:5173") + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -21,6 +32,8 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); +app.UseCors("AllowFrontend"); + app.UseAuthorization(); app.MapControllers();