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'}
+ >
+
+
+
+ );
+}
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();