# Guía Completa: Módulos, Menús y Permisos en SLM Framework

> **Versión:** 1.0
> **Última actualización:** 2026-01-22
> **Audiencia:** Equipo de desarrollo (15 desarrolladores)

---

## 📋 Tabla de Contenidos

1. [Referencia Rápida](#-referencia-rápida)
2. [Introducción al Sistema](#-introducción-al-sistema)
3. [Arquitectura del Sistema](#-arquitectura-del-sistema)
4. [Módulos](#-módulos)
5. [Sistema de Menús](#-sistema-de-menús)
6. [Perfiles y Permisos (ACL)](#-perfiles-y-permisos-acl)
7. [Flujo Completo de Implementación](#-flujo-completo-de-implementación)
8. [Ejemplos Prácticos](#-ejemplos-prácticos)
9. [Mejores Prácticas](#-mejores-prácticas)
10. [Troubleshooting](#-troubleshooting)

---

## 🚀 Referencia Rápida

| Concepto | Tipo ES | Propósito | Ubicación |
|----------|---------|-----------|-----------|
| **Módulo** | `module` | Define una funcionalidad del sistema | `appweb/slm.framework/modules/{directorio}/` |
| **Menú Simple** | `simple_menu` | Item de menú que apunta a un módulo | Elasticsearch |
| **Menú Desplegable** | `dropdown_menu` | Item de menú con submódulos | Elasticsearch |
| **Grupo de Menú** | `group_menu` | Sección del sidebar que agrupa menús | Elasticsearch |
| **Perfil** | `profile` | Define permisos y acceso para usuarios | Elasticsearch |
| **Usuario** | `user` | Usuario del sistema asociado a un perfil | Elasticsearch |

---

## 📖 Introducción al Sistema

El SLM Framework utiliza un sistema modular basado en Elasticsearch que permite:

- ✅ Crear módulos independientes con funcionalidades específicas
- ✅ Organizar el sidebar mediante grupos, menús y submenús
- ✅ Controlar el acceso mediante perfiles y ACL (Access Control List)
- ✅ Gestionar permisos granulares (read, create, update, delete)
- ✅ Configurar todo dinámicamente sin modificar código

### Ventajas del Sistema

1. **Modularidad**: Cada funcionalidad vive en su propio módulo
2. **Flexibilidad**: Configuración en Elasticsearch sin redeployments
3. **Seguridad**: Control de acceso a nivel de módulo y acción
4. **Escalabilidad**: Fácil agregar nuevos módulos y permisos

---

## 🏗 Arquitectura del Sistema

### Diagrama de Relaciones

```mermaid
graph TB
    U[Usuario] -->|tiene asignado| P[Perfil]
    P -->|contiene| ACL[ACL]
    ACL -->|referencia| GM[Grupo de Menú]
    GM -->|contiene| SM[Menú Simple]
    GM -->|contiene| DM[Menú Desplegable]
    SM -->|apunta a| MOD[Módulo]
    DM -->|contiene múltiples| MOD
    MOD -->|define permisos para| ACL

    style U fill:#e1f5ff
    style P fill:#fff3cd
    style ACL fill:#f8d7da
    style GM fill:#d4edda
    style SM fill:#d1ecf1
    style DM fill:#d1ecf1
    style MOD fill:#e2e3e5
```

### Flujo de Autorización

```mermaid
sequenceDiagram
    participant Usuario
    participant Sistema
    participant Perfil
    participant ACL
    participant Módulo

    Usuario->>Sistema: Solicita acceso a módulo
    Sistema->>Perfil: Obtiene perfil del usuario
    Perfil->>ACL: Consulta permisos
    ACL->>Módulo: Verifica acceso al módulo
    Módulo-->>Sistema: Retorna permisos disponibles
    Sistema-->>Usuario: Permite/Deniega acceso
```

---

## 🧩 Módulos

### ¿Qué es un Módulo?

Un módulo es una unidad funcional del sistema que agrupa:
- Archivos de vista (HTML)
- Lógica de negocio (AJAX handlers)
- Pre-procesamiento y carga de datos
- JavaScript específico del módulo
- Funciones auxiliares (API del módulo)

### Estructura de Archivos de un Módulo

```
appweb/slm.framework/modules/
└── nombre_modulo/
    ├── slm_api.slm          # Funciones reutilizables del módulo (opcional)
    ├── proc.accion.slm      # Pre-procesamiento antes de cargar vista (opcional)
    ├── load.accion.slm      # Carga de datos para la vista (opcional)
    ├── view.accion.slm      # Vista HTML (REQUERIDO)
    ├── ajax.accion.slm      # Handlers AJAX (opcional)
    └── end.accion.slm       # JavaScript del módulo (opcional)
```

### Orden de Carga de Archivos

#### Para Vistas (`?v=modulo.accion`)
```
1. slm_api.slm        → Funciones del módulo
2. proc.accion.slm    → Pre-procesamiento
3. load.accion.slm    → Carga de datos
4. _header.slm        → Encabezado de página
5. view.accion.slm    → Contenido principal
6. _footer.slm        → Pie de página
   └─ end.accion.slm  → JavaScript (incluido en footer)
```

#### Para AJAX (`?a=modulo.accion`)
```
1. slm_api.slm        → Funciones del módulo
2. ajax.accion.slm    → Procesa solicitud y retorna JSON
```

---

## 📂 Configuración de Módulos

### Registro en Elasticsearch

Los módulos se registran en Elasticsearch con el tipo `module`. Esto permite gestionarlos dinámicamente sin modificar código.

#### Ubicación del Módulo de Configuración
```
appweb/slm.framework/modules/config/
├── view.modules.slm      # Interfaz de administración
├── ajax.modules.slm      # Lógica de CRUD de módulos
├── load.modules.slm      # Carga datos para la vista
└── slm_api.slm          # Funciones auxiliares
```

### Estructura de un Documento de Módulo

```json
{
  "_id": "dYg4L5EBd3McsM5dxTQM",
  "name": "Inicio",
  "directory": "home",
  "file": "main",
  "folder": "home.main",
  "description": "Página principal del sistema",
  "status": "active",
  "slm": {
    "type": "module",
    "timestamp": 1723074135307
  }
}
```

#### Campos Explicados

| Campo | Tipo | Requerido | Descripción |
|-------|------|-----------|-------------|
| `name` | string | ✅ | Nombre legible del módulo |
| `directory` | string | ✅ | Nombre del directorio físico |
| `file` | string | ✅ | Nombre base de los archivos (sin extensión) |
| `folder` | string | ✅ | Identificador único del módulo (`directory.file`) |
| `description` | string | ❌ | Descripción del propósito del módulo |
| `status` | string | ✅ | Estado: `active` o `inactive` |
| `slm.type` | string | ✅ | Siempre debe ser `"module"` |
| `slm.timestamp` | number | ✅ | Timestamp de creación en milisegundos |

---

### Proceso de Registro de Módulos

#### Opción 1: Mediante Interfaz Web

1. Acceder a **Configuración → Módulos**
2. Crear archivos físicos del módulo en `appweb/slm.framework/modules/`
3. Hacer clic en **"Registrar módulo"**
4. El sistema detecta automáticamente los módulos no registrados
5. Completar formulario y guardar

#### Opción 2: Mediante Elasticsearch (Manual)

```bash
POST /slm-app-dev/_doc/
Content-Type: application/json

{
  "name": "Catálogo de Productos",
  "directory": "productos",
  "file": "catalogo",
  "folder": "productos.catalogo",
  "description": "Gestión del catálogo de productos",
  "status": "active",
  "slm": {
    "type": "module",
    "timestamp": 1723074135307
  }
}
```

#### Opción 3: Mediante Función PHP

```php
<?php
// En ajax.modules.slm - función registerModule()

function registerModule(): void
{
    validateRequiredFields(['name', 'directory', 'file', 'folder']);

    $document = [
        'name' => getPostString('name'),
        'directory' => getPostString('directory'),
        'file' => getPostString('file'),
        'folder' => getPostString('folder'),
        'status' => 'active',
        'slm' => [
            'type' => 'module',
            'timestamp' => getTimestamp()
        ]
    ];

    $result = setNewObject($document);

    if (($result['result'] ?? '') !== 'created') {
        throw new Exception('No se pudo crear el registro');
    }

    jsonSuccess('Módulo registrado correctamente', ['id' => $result['_id']]);
}
```

---

### Ejemplo Completo: Crear Módulo de Inventario

#### Paso 1: Crear Estructura de Archivos

```bash
mkdir -p appweb/slm.framework/modules/inventario
cd appweb/slm.framework/modules/inventario
touch view.productos.slm ajax.productos.slm load.productos.slm end.productos.slm slm_api.slm
```

#### Paso 2: Implementar Vista (`view.productos.slm`)

```php
<?php
/**
 * Vista de Productos - Inventario
 */
?>
<div class="app-content">
    <div class="side-app">
        <!-- PAGE-HEADER -->
        <div class="page-header">
            <div>
                <h1 class="page-title">Inventario</h1>
                <ol class="breadcrumb">
                    <li class="breadcrumb-item"><a href="#"><?= APP_NAME ?></a></li>
                    <li class="breadcrumb-item active">Productos</li>
                </ol>
            </div>
            <div class="ms-auto pageheader-btn">
                <?php if (in_array('create', $permissions)): ?>
                <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#productoModal">
                    <i class="fe fe-plus"></i> Nuevo Producto
                </button>
                <?php endif; ?>
            </div>
        </div>
        <!-- PAGE-HEADER END -->

        <!-- CONTENT -->
        <div class="row">
            <div class="col-lg-12">
                <div class="card">
                    <div class="card-body">
                        <div class="table-responsive">
                            <table id="productosTable" class="table table-bordered text-nowrap w-100">
                                <thead>
                                    <tr>
                                        <th>SKU</th>
                                        <th>Nombre</th>
                                        <th>Categoría</th>
                                        <th>Stock</th>
                                        <th>Precio</th>
                                        <th>Estado</th>
                                        <th>Acciones</th>
                                    </tr>
                                </thead>
                            </table>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Modal de Producto -->
<div class="modal fade" id="productoModal" tabindex="-1">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Producto</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <form id="productoForm">
                <div class="modal-body">
                    <input type="hidden" name="producto_id">

                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label class="form-label">SKU *</label>
                            <input type="text" class="form-control" name="sku" required>
                        </div>
                        <div class="col-md-6 mb-3">
                            <label class="form-label">Nombre *</label>
                            <input type="text" class="form-control" name="name" required>
                        </div>
                    </div>

                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label class="form-label">Categoría</label>
                            <select class="form-control form-select" name="category">
                                <option value="">Seleccione...</option>
                                <?php foreach ($categories as $cat): ?>
                                <option value="<?= $cat['id'] ?>"><?= $cat['name'] ?></option>
                                <?php endforeach; ?>
                            </select>
                        </div>
                        <div class="col-md-3 mb-3">
                            <label class="form-label">Stock *</label>
                            <input type="number" class="form-control" name="stock" required>
                        </div>
                        <div class="col-md-3 mb-3">
                            <label class="form-label">Precio *</label>
                            <input type="number" class="form-control" name="price" step="0.01" required>
                        </div>
                    </div>

                    <div class="mb-3">
                        <label class="form-label">Descripción</label>
                        <textarea class="form-control" name="description" rows="3"></textarea>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
                    <button type="submit" class="btn btn-primary">Guardar</button>
                </div>
            </form>
        </div>
    </div>
</div>
```

#### Paso 3: Implementar Carga de Datos (`load.productos.slm`)

```php
<?php
/**
 * Carga de datos para vista de productos
 */

// Verificar permisos del usuario
$permissions = _SLM::getSecurityAccess('inventario.productos');

// Cargar categorías para el dropdown
$es_index = getEsIndex();
$result = json_decode(
    es_execute("/$es_index/_search?q=slm.type:category&size=100&sort=name.keyword:asc"),
    true
);

$categories = [];
if (!isset($result['error']) && $result['hits']['total']['value'] > 0) {
    $categories = array_map(function($hit) {
        return array_merge(['id' => $hit['_id']], $hit['_source']);
    }, $result['hits']['hits']);
}

// Variables disponibles en la vista
$canCreate = in_array('create', $permissions);
$canUpdate = in_array('update', $permissions);
$canDelete = in_array('delete', $permissions);
```

#### Paso 4: Implementar AJAX Handlers (`ajax.productos.slm`)

```php
<?php
/**
 * AJAX Handlers para módulo de productos
 */

include_once "slm_api.slm";

// Dispatcher
if (isset($_POST['acc'])) {
    $function = $_POST['acc'];
    if (!function_exists($function)) {
        jsonError('Acción no válida');
    }
    $function();
}

/**
 * Obtiene lista de productos para DataTable
 */
function getProductos(): void
{
    $es_index = getEsIndex();
    $searchTerm = $_POST['search']['value'] ?? '';
    $start = (int)($_POST['start'] ?? 0);
    $length = (int)($_POST['length'] ?? 10);
    $draw = (int)($_POST['draw'] ?? 1);

    $search = ['(slm.type:producto)'];
    $fields = ['sku.keyword', 'name', 'description'];

    $params = [
        'from' => $start,
        'size' => $length,
        'sort' => 'slm.timestamp',
        'order' => 'desc'
    ];

    try {
        $ssp = new SSP($searchTerm, $search, $fields, $params);
        $result = $ssp->setEsIndex($es_index)->setReturn(true)->run();

        jsonSuccess('', [
            'draw' => $draw,
            'recordsTotal' => $result['recordsTotal'],
            'recordsFiltered' => $result['recordsFiltered'],
            'data' => $result['data']
        ]);
    } catch (Exception $e) {
        jsonError($e->getMessage(), [
            'draw' => $draw,
            'recordsTotal' => 0,
            'recordsFiltered' => 0,
            'data' => []
        ]);
    }
}

/**
 * Crea un nuevo producto
 */
function createProducto(): void
{
    $response = _response(true);
    $logType = 'error';
    $logData = [];

    try {
        // Verificar permisos
        $permissions = _SLM::getSecurityAccess('inventario.productos');
        if (!in_array('create', $permissions)) {
            throw new Exception('No tiene permisos para crear productos');
        }

        // Validar campos requeridos
        validateRequiredFields(['sku', 'name', 'stock', 'price']);

        // Verificar que el SKU no exista
        $existingSku = getObjectWithValue('producto', 'sku.keyword', getPostString('sku'));
        if ($existingSku) {
            throw new Exception('El SKU ya existe en el sistema');
        }

        // Preparar documento
        $document = [
            'sku' => strtoupper(trim(getPostString('sku'))),
            'name' => getPostString('name'),
            'description' => getPostString('description', ''),
            'category' => getPostString('category', ''),
            'stock' => (int)getPostValue('stock', 0),
            'price' => (float)getPostValue('price', 0),
            'status' => 'active',
            'slm' => [
                'type' => 'producto',
                'timestamp' => getTimestamp()
            ]
        ];

        // Crear en Elasticsearch
        $result = setNewObject($document);

        if (isset($result['error']) || ($result['result'] ?? '') !== 'created') {
            throw new Exception('Error al crear el producto');
        }

        $response = _response(false, 'Producto creado exitosamente', [
            'id' => $result['_id'],
            'closeModal' => true,
            'reloadTable' => true
        ]);
        $logType = 'success';

    } catch (Exception $e) {
        $response['message'] = $e->getMessage();
    } finally {
        $logData['response'] = $response;
        addToLog($logType, 'inventario', __FUNCTION__, $logData);
    }

    jsonResponse($response);
}

/**
 * Actualiza un producto existente
 */
function updateProducto(): void
{
    $response = _response(true);
    $logType = 'error';
    $logData = [];

    try {
        // Verificar permisos
        $permissions = _SLM::getSecurityAccess('inventario.productos');
        if (!in_array('update', $permissions)) {
            throw new Exception('No tiene permisos para actualizar productos');
        }

        // Validar campos
        validateRequiredFields(['producto_id', 'sku', 'name', 'stock', 'price']);

        $id = getPostString('producto_id');

        // Preparar actualización
        $document = [
            'sku' => strtoupper(trim(getPostString('sku'))),
            'name' => getPostString('name'),
            'description' => getPostString('description', ''),
            'category' => getPostString('category', ''),
            'stock' => (int)getPostValue('stock', 0),
            'price' => (float)getPostValue('price', 0),
            'slm' => [
                'type' => 'producto',
                'timestamp' => getTimestamp()
            ]
        ];

        // Actualizar en Elasticsearch
        $result = updateObjectByID($id, $document);

        if (isset($result['error']) || !in_array($result['result'] ?? '', ['updated', 'noop'])) {
            throw new Exception('Error al actualizar el producto');
        }

        $response = _response(false, 'Producto actualizado exitosamente', [
            'closeModal' => true,
            'reloadTable' => true
        ]);
        $logType = 'success';

    } catch (Exception $e) {
        $response['message'] = $e->getMessage();
    } finally {
        $logData['response'] = $response;
        addToLog($logType, 'inventario', __FUNCTION__, $logData);
    }

    jsonResponse($response);
}

/**
 * Elimina un producto (soft delete)
 */
function deleteProducto(): void
{
    $response = _response(true);
    $logType = 'error';
    $logData = [];

    try {
        // Verificar permisos
        $permissions = _SLM::getSecurityAccess('inventario.productos');
        if (!in_array('delete', $permissions)) {
            throw new Exception('No tiene permisos para eliminar productos');
        }

        $id = getPostString('producto_id');
        if (empty($id)) {
            throw new InvalidArgumentException('ID del producto requerido');
        }

        // Soft delete - cambiar estado a inactive
        $result = updateObjectByID($id, [
            'status' => 'inactive',
            'slm' => [
                'type' => 'producto',
                'timestamp' => getTimestamp()
            ]
        ]);

        if (isset($result['error'])) {
            throw new Exception('Error al eliminar producto');
        }

        $response = _response(false, 'Producto eliminado exitosamente', [
            'reloadTable' => true
        ]);
        $logType = 'success';

    } catch (Exception $e) {
        $response['message'] = $e->getMessage();
    } finally {
        $logData['response'] = $response;
        addToLog($logType, 'inventario', __FUNCTION__, $logData);
    }

    jsonResponse($response);
}

/**
 * Obtiene un producto por ID
 */
function getProducto(): void
{
    try {
        $id = getPostString('producto_id');
        if (empty($id)) {
            throw new InvalidArgumentException('ID del producto requerido');
        }

        $producto = getObjectById($id);
        if (!$producto) {
            throw new Exception('Producto no encontrado');
        }

        jsonSuccess('', ['producto' => array_merge(['id' => $id], $producto)]);

    } catch (Exception $e) {
        jsonError($e->getMessage());
    }
}
```

#### Paso 5: Implementar JavaScript (`end.productos.slm`)

```html
<script>
/**
 * JavaScript para módulo de productos
 */

// Configuración del módulo
SLM.module = './?a=inventario.productos';

let productosTable;
const productoModal = new bootstrap.Modal(document.getElementById('productoModal'));

// Inicializar DataTable
$(document).ready(function() {
    productosTable = $('#productosTable').DataTable({
        processing: true,
        serverSide: true,
        ajax: {
            url: SLM.module,
            type: 'POST',
            data: function(d) {
                d.acc = 'getProductos';
            }
        },
        columns: [
            { data: 'sku' },
            { data: 'name' },
            {
                data: 'category',
                render: function(data, type, row) {
                    return data || '<em class="text-muted">Sin categoría</em>';
                }
            },
            {
                data: 'stock',
                render: function(data) {
                    const stockClass = data > 10 ? 'success' : data > 0 ? 'warning' : 'danger';
                    return `<span class="badge bg-${stockClass}">${data}</span>`;
                }
            },
            {
                data: 'price',
                render: function(data) {
                    return '$' + parseFloat(data || 0).toFixed(2);
                }
            },
            {
                data: 'status',
                render: function(data) {
                    return data === 'active'
                        ? '<span class="badge bg-success">Activo</span>'
                        : '<span class="badge bg-danger">Inactivo</span>';
                }
            },
            {
                data: null,
                orderable: false,
                render: function(data, type, row) {
                    let buttons = '';

                    buttons += `
                        <button class="btn btn-sm btn-outline-info" onclick="viewProducto('${row.id}')" title="Ver">
                            <i data-feather="eye" style="width:14px;"></i>
                        </button>
                    `;

                    <?php if ($canUpdate): ?>
                    buttons += `
                        <button class="btn btn-sm btn-outline-primary" onclick="editProducto('${row.id}')" title="Editar">
                            <i data-feather="edit" style="width:14px;"></i>
                        </button>
                    `;
                    <?php endif; ?>

                    <?php if ($canDelete): ?>
                    buttons += `
                        <button class="btn btn-sm btn-outline-danger" onclick="deleteProducto('${row.id}')" title="Eliminar">
                            <i data-feather="trash-2" style="width:14px;"></i>
                        </button>
                    `;
                    <?php endif; ?>

                    return buttons;
                }
            }
        ],
        language: {
            url: '//cdn.datatables.net/plug-ins/1.13.4/i18n/es-ES.json'
        },
        drawCallback: function() {
            feather.replace();
        }
    });
});

// Limpiar formulario al abrir modal
$('#productoModal').on('show.bs.modal', function (e) {
    if (!$(e.relatedTarget).data('edit')) {
        document.getElementById('productoForm').reset();
        $('input[name="producto_id"]').val('');
    }
});

// Manejar envío del formulario
document.getElementById('productoForm').addEventListener('submit', async function(e) {
    e.preventDefault();

    const id = $('input[name="producto_id"]').val();
    const formData = new FormData(this);
    const data = {};
    formData.forEach((value, key) => data[key] = value);

    try {
        SLM.loading('Guardando producto...');

        const action = id ? 'updateProducto' : 'createProducto';
        const result = await SLM.ajax(action, data);

        SLM.closeLoading();
        SLM.success(result.message);

        if (result.closeModal) productoModal.hide();
        if (result.reloadTable) productosTable.ajax.reload();

    } catch (error) {
        SLM.closeLoading();
        SLM.error(error.message || 'Error al guardar el producto');
    }
});

/**
 * Ver detalles de un producto
 */
async function viewProducto(id) {
    try {
        SLM.loading('Cargando producto...');
        const result = await SLM.ajax('getProducto', { producto_id: id });
        SLM.closeLoading();

        const producto = result.producto;

        // Mostrar información en modal o página
        Swal.fire({
            title: producto.name,
            html: `
                <div class="text-start">
                    <p><strong>SKU:</strong> ${producto.sku}</p>
                    <p><strong>Stock:</strong> ${producto.stock}</p>
                    <p><strong>Precio:</strong> $${parseFloat(producto.price).toFixed(2)}</p>
                    <p><strong>Descripción:</strong> ${producto.description || 'Sin descripción'}</p>
                </div>
            `,
            icon: 'info'
        });

    } catch (error) {
        SLM.closeLoading();
        SLM.error(error.message || 'Error al cargar el producto');
    }
}

/**
 * Editar producto
 */
async function editProducto(id) {
    try {
        SLM.loading('Cargando producto...');
        const result = await SLM.ajax('getProducto', { producto_id: id });
        SLM.closeLoading();

        const producto = result.producto;

        // Llenar formulario
        $('input[name="producto_id"]').val(producto.id);
        $('input[name="sku"]').val(producto.sku);
        $('input[name="name"]').val(producto.name);
        $('textarea[name="description"]').val(producto.description || '');
        $('select[name="category"]').val(producto.category || '');
        $('input[name="stock"]').val(producto.stock);
        $('input[name="price"]').val(producto.price);

        productoModal.show();

    } catch (error) {
        SLM.closeLoading();
        SLM.error(error.message || 'Error al cargar el producto');
    }
}

/**
 * Eliminar producto
 */
async function deleteProducto(id) {
    const result = await SLM.confirm(
        '¿Está seguro de eliminar este producto?',
        'Esta acción no se puede deshacer'
    );

    if (!result.isConfirmed) return;

    try {
        SLM.loading('Eliminando producto...');
        const response = await SLM.ajax('deleteProducto', { producto_id: id });
        SLM.closeLoading();

        SLM.success(response.message);
        if (response.reloadTable) productosTable.ajax.reload();

    } catch (error) {
        SLM.closeLoading();
        SLM.error(error.message || 'Error al eliminar el producto');
    }
}
</script>
```

#### Paso 6: Registrar Módulo en Elasticsearch

```bash
POST /slm-app-dev/_doc/
Content-Type: application/json

{
  "name": "Productos",
  "directory": "inventario",
  "file": "productos",
  "folder": "inventario.productos",
  "description": "Gestión del catálogo de productos del inventario",
  "status": "active",
  "slm": {
    "type": "module",
    "timestamp": 1737582000000
  }
}
```

**Respuesta:**
```json
{
  "_id": "XYZ123ABC456",
  "result": "created"
}
```

---

## 🗂 Sistema de Menús

El sistema de menús en SLM Framework está compuesto por tres tipos de elementos que se organizan jerárquicamente:

### Tipos de Elementos de Menú

#### 1. Menú Simple (`simple_menu`)

Un menú simple apunta directamente a un módulo específico.

**Estructura:**
```json
{
  "_id": "CYhIL5EBd3McsM5dJznL",
  "name": "Inicio",
  "icon": "home",
  "folder": "dYg4L5EBd3McsM5dxTQM",
  "display": "true",
  "status": "active",
  "slm": {
    "type": "simple_menu",
    "timestamp": 1723075143626
  }
}
```

**Campos:**
- `name`: Texto que se muestra en el menú
- `icon`: Clase CSS del icono (ej: `"fe fe-home"`, `"home"`)
- `folder`: ID del módulo al que apunta
- `display`: Si se muestra o no (`"true"` / `"false"`)
- `status`: Estado del menú (`"active"` / `"inactive"`)

#### 2. Menú Desplegable (`dropdown_menu`)

Un menú desplegable agrupa múltiples módulos bajo un mismo item.

**Estructura:**
```json
{
  "_id": "FSQLuZEBz4WfLYztf8nY",
  "name": "Configuración",
  "icon": "settings",
  "display": "true",
  "module": [
    {
      "folder": "4IeOKpEBd3McsM5duoOs",
      "display": ["true"]
    },
    {
      "folder": "7YeOKpEBd3McsM5d6YNd",
      "display": ["true"]
    },
    {
      "folder": "2oeOKpEBd3McsM5dg4NN",
      "display": ["true"]
    }
  ],
  "status": "active",
  "slm": {
    "type": "dropdown_menu",
    "timestamp": 1725386424279
  }
}
```

**Campos:**
- `name`: Nombre del menú desplegable
- `icon`: Icono principal
- `display`: Visibilidad del menú
- `module`: Array de submódulos
  - `folder`: ID del módulo
  - `display`: Visibilidad del submódulo
- `status`: Estado del menú

#### 3. Grupo de Menú (`group_menu`)

Un grupo de menú agrupa varios menús (simples o desplegables) bajo una sección del sidebar.

**Estructura:**
```json
{
  "_id": "MiQMuZEBz4WfLYztUMlY",
  "title": "Configuración",
  "order": 99,
  "menu": [
    {
      "item": "FSQLuZEBz4WfLYztf8nY"
    }
  ],
  "status": "active",
  "display": "true",
  "slm": {
    "type": "group_menu",
    "timestamp": 1725386477656
  }
}
```

**Campos:**
- `title`: Título del grupo (se muestra como encabezado en el sidebar)
- `order`: Orden de aparición (menor número = más arriba)
- `menu`: Array de items del menú
  - `item`: ID del menú (simple o desplegable)
- `display`: Visibilidad del grupo
- `status`: Estado del grupo

---

### Jerarquía del Sidebar

```
📁 Grupo de Menú (group_menu) - order: 0
  └─ 📄 Menú Simple (simple_menu)
      → Módulo: home.main

📁 Grupo de Menú (group_menu) - order: 10
  └─ 📁 Menú Desplegable (dropdown_menu)
      ├─ Submódulo: inventario.productos
      ├─ Submódulo: inventario.categorias
      └─ Submódulo: inventario.proveedores

📁 Grupo de Menú (group_menu) - order: 99
  └─ 📁 Menú Desplegable (dropdown_menu)
      ├─ Submódulo: config.profile
      ├─ Submódulo: config.users
      └─ Submódulo: config.modules
```

---

### Crear Elementos de Menú

#### Ejemplo 1: Menú Simple para Productos

```json
POST /slm-app-dev/_doc/

{
  "name": "Productos",
  "icon": "fe fe-package",
  "folder": "XYZ123ABC456",
  "display": "true",
  "status": "active",
  "slm": {
    "type": "simple_menu",
    "timestamp": 1737582100000
  }
}
```

#### Ejemplo 2: Menú Desplegable para Inventario

```json
POST /slm-app-dev/_doc/

{
  "name": "Inventario",
  "icon": "fe fe-archive",
  "display": "true",
  "module": [
    {
      "folder": "XYZ123ABC456",
      "display": ["true"]
    },
    {
      "folder": "ABC456XYZ789",
      "display": ["true"]
    },
    {
      "folder": "DEF789GHI012",
      "display": ["true"]
    }
  ],
  "status": "active",
  "slm": {
    "type": "dropdown_menu",
    "timestamp": 1737582200000
  }
}
```

#### Ejemplo 3: Grupo de Menú para Gestión

```json
POST /slm-app-dev/_doc/

{
  "title": "Gestión",
  "order": 50,
  "menu": [
    {
      "item": "MENU_INVENTARIO_ID"
    },
    {
      "item": "MENU_CLIENTES_ID"
    }
  ],
  "status": "active",
  "display": "true",
  "slm": {
    "type": "group_menu",
    "timestamp": 1737582300000
  }
}
```

---

## 🔐 Perfiles y Permisos (ACL)

### Sistema de Control de Acceso

El sistema ACL (Access Control List) de SLM Framework proporciona control granular sobre qué usuarios pueden acceder a qué módulos y qué acciones pueden realizar.

### Estructura de un Perfil

```json
{
  "_id": "ZYNuIogBygoTW4EQXOl4",
  "name": "SLM Super Admin",
  "description": "Perfil para administradores del sistema",
  "status": "active",
  "default_view": "home.main",
  "acl": {
    "niTMtJEBz4WfLYzt0ooT": {
      "home.main": ["read", "create", "delete", "update"]
    },
    "MiQMuZEBz4WfLYztUMlY": {
      "config.profile": ["read", "create", "delete", "update"],
      "config.users": ["read", "create", "delete", "update"],
      "config.modules": ["read", "create", "delete", "update"]
    },
    "GHI789JKL012": {
      "inventario.productos": ["read", "create", "update"],
      "inventario.categorias": ["read"]
    }
  },
  "slm": {
    "type": "profile",
    "timestamp": 1720627238782
  }
}
```

#### Campos de un Perfil

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `name` | string | Nombre del perfil |
| `description` | string | Descripción del propósito del perfil |
| `status` | string | Estado: `"active"` o `"inactive"` |
| `default_view` | string | Módulo de inicio al hacer login |
| `acl` | object | Estructura de permisos (ver detalle abajo) |
| `slm.type` | string | Siempre `"profile"` |

### Estructura del ACL

El ACL es un objeto donde:
- **Clave**: ID del grupo de menú (`group_menu`)
- **Valor**: Objeto con módulos y sus permisos

```json
{
  "acl": {
    "GROUP_MENU_ID": {
      "modulo.accion": ["permiso1", "permiso2", "..."]
    }
  }
}
```

### Tipos de Permisos

| Permiso | Descripción | Uso Típico |
|---------|-------------|------------|
| `read` | Lectura / Consulta | Ver listas, consultar registros |
| `create` | Creación | Crear nuevos registros |
| `update` | Actualización | Modificar registros existentes |
| `delete` | Eliminación | Eliminar (soft o hard delete) registros |

> **Importante:** Los permisos son acumulativos. Un usuario con `["read", "create"]` puede leer y crear, pero no actualizar ni eliminar.

---

### Verificación de Permisos en el Código

#### En Archivos PHP

```php
<?php
// Obtener permisos del usuario para el módulo actual
$permissions = _SLM::getSecurityAccess('inventario.productos');

// Verificar si tiene permiso de lectura
if (in_array('read', $permissions)) {
    // Mostrar datos
}

// Verificar si tiene permiso de creación
if (in_array('create', $permissions)) {
    // Mostrar botón "Nuevo"
}

// En handlers AJAX - SIEMPRE verificar permisos
function createProducto(): void
{
    $permissions = _SLM::getSecurityAccess('inventario.productos');

    if (!in_array('create', $permissions)) {
        jsonError('No tiene permisos para crear productos');
    }

    // Continuar con la lógica...
}
```

#### En Vistas HTML

```php
<div class="ms-auto pageheader-btn">
    <?php if (in_array('create', $permissions)): ?>
    <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modal">
        <i class="fe fe-plus"></i> Nuevo
    </button>
    <?php endif; ?>
</div>
```

#### En JavaScript

```javascript
// Los permisos se inyectan desde PHP en la vista
const canCreate = <?= in_array('create', $permissions) ? 'true' : 'false' ?>;
const canUpdate = <?= in_array('update', $permissions) ? 'true' : 'false' ?>;
const canDelete = <?= in_array('delete', $permissions) ? 'true' : 'false' ?>;

// Uso en funciones
function renderActions(data, type, row) {
    let buttons = '';

    if (canUpdate) {
        buttons += `<button onclick="edit('${row.id}')">Editar</button>`;
    }

    if (canDelete) {
        buttons += `<button onclick="remove('${row.id}')">Eliminar</button>`;
    }

    return buttons;
}
```

---

### Ejemplos de Perfiles Comunes

#### 1. Perfil de Administrador

```json
{
  "name": "Administrador General",
  "description": "Acceso completo al sistema",
  "status": "active",
  "default_view": "home.main",
  "acl": {
    "GROUP_INICIO_ID": {
      "home.main": ["read"]
    },
    "GROUP_GESTION_ID": {
      "inventario.productos": ["read", "create", "update", "delete"],
      "inventario.categorias": ["read", "create", "update", "delete"],
      "clientes.lista": ["read", "create", "update", "delete"]
    },
    "GROUP_CONFIG_ID": {
      "config.profile": ["read", "create", "update", "delete"],
      "config.users": ["read", "create", "update", "delete"],
      "config.modules": ["read", "create", "update", "delete"]
    }
  },
  "slm": {
    "type": "profile",
    "timestamp": 1737582400000
  }
}
```

#### 2. Perfil de Operador

```json
{
  "name": "Operador de Inventario",
  "description": "Gestión de productos sin permisos de eliminación",
  "status": "active",
  "default_view": "inventario.productos",
  "acl": {
    "GROUP_INICIO_ID": {
      "home.main": ["read"]
    },
    "GROUP_GESTION_ID": {
      "inventario.productos": ["read", "create", "update"],
      "inventario.categorias": ["read"]
    }
  },
  "slm": {
    "type": "profile",
    "timestamp": 1737582500000
  }
}
```

#### 3. Perfil de Consulta (Solo Lectura)

```json
{
  "name": "Consulta General",
  "description": "Acceso de solo lectura a módulos básicos",
  "status": "active",
  "default_view": "home.main",
  "acl": {
    "GROUP_INICIO_ID": {
      "home.main": ["read"]
    },
    "GROUP_GESTION_ID": {
      "inventario.productos": ["read"],
      "inventario.categorias": ["read"],
      "clientes.lista": ["read"]
    }
  },
  "slm": {
    "type": "profile",
    "timestamp": 1737582600000
  }
}
```

---

### Asignar Perfil a Usuario

Un usuario se asocia a un perfil mediante el campo `profile`:

```json
{
  "_id": "USER123ABC",
  "name": "Juan",
  "lastname": "Pérez",
  "email": "juan.perez@empresa.com",
  "profile": "ZYNuIogBygoTW4EQXOl4",
  "status": "active",
  "email_confirmed": true,
  "country_code": "+52",
  "telephone_number": "5512345678",
  "password": "HASHED_PASSWORD_HERE",
  "slm": {
    "type": "user",
    "timestamp": 1720631439332
  }
}
```

> **Importante:** El campo `profile` debe contener el `_id` de un perfil existente.

---

### Funciones de Menú (Sistema Actualizado)

El sistema de menús utiliza dos funciones principales para construir y filtrar el sidebar:

#### 1. `fetchGroupMenus()` - Obtiene todos los grupos de menú

Esta función recupera todos los grupos de menú desde Elasticsearch sin aplicar filtros de permisos.

**Uso:**
```php
<?php
// Obtener todos los grupos de menú
$groups = fetchGroupMenus();

// Retorna un array con la estructura completa de grupos, menús y módulos
```

**Características:**
- Obtiene todos los `group_menu` activos de Elasticsearch
- Resuelve las referencias de menús (`simple_menu` y `dropdown_menu`)
- Resuelve las referencias de módulos
- No aplica filtros de permisos
- Retorna la estructura completa del menú

#### 2. `filterMenuWithPermissions()` - Filtra menú según permisos

Esta función toma los grupos obtenidos por `fetchGroupMenus()` y los filtra según el ACL del usuario.

**Uso:**
```php
<?php
// Obtener grupos
$groups = fetchGroupMenus();

// Filtrar según permisos del usuario actual
$menu = filterMenuWithPermissions($groups);

// O filtrar con ACL específico
$menu = filterMenuWithPermissions($groups, $customAcl);
```

**Características:**
- Filtra grupos, menús y módulos según el ACL
- Verifica permisos de lectura (`read`) para mostrar elementos
- Mantiene la estructura jerárquica
- Omite elementos sin permisos

**Ejemplo completo:**
```php
<?php
// En load.accion.slm

// Obtener menú filtrado para el usuario actual
$groups = fetchGroupMenus();
$userMenu = filterMenuWithPermissions($groups);

// Verificar si el usuario tiene acceso a un grupo específico
$hasCrmAccess = false;
foreach ($userMenu as $group) {
    if ($group['title'] === 'CRM') {
        $hasCrmAccess = true;
        break;
    }
}
```

#### Migración desde `getMenu()`

Si tienes código que usa la función deprecada `getMenu()`, actualízalo así:

**Antes (deprecado):**
```php
<?php
// Obtener menú con permisos
$menu = getMenu(false);

// Obtener todos los menús sin filtrar
$allMenus = getMenu(true);

// Obtener menú con ACL específico
$menu = getMenu(false, $customAcl);
```

**Después (nuevo sistema):**
```php
<?php
// Obtener menú con permisos del usuario actual
$groups = fetchGroupMenus();
$menu = filterMenuWithPermissions($groups);

// Obtener todos los menús sin filtrar
$allMenus = fetchGroupMenus();

// Obtener menú con ACL específico
$groups = fetchGroupMenus();
$menu = filterMenuWithPermissions($groups, $customAcl);
```

**Ventajas del nuevo sistema:**
- ✅ Separación de responsabilidades (fetch vs filter)
- ✅ Mejor rendimiento (cache de grupos)
- ✅ Más flexible para testing
- ✅ Código más mantenible

---

## 🔄 Flujo Completo de Implementación

### Flujo: Desde Crear un Módulo hasta Verlo en el Sidebar

```mermaid
graph TD
    A[1. Crear archivos del módulo] --> B[2. Registrar módulo en ES]
    B --> C[3. Crear elemento de menú]
    C --> D{Tipo de menú?}
    D -->|Simple| E[Crear simple_menu]
    D -->|Con submenús| F[Crear dropdown_menu]
    E --> G[4. Agregar a grupo de menú]
    F --> G
    G --> H[5. Configurar perfil con ACL]
    H --> I[6. Asignar perfil a usuario]
    I --> J[7. Usuario ve módulo en sidebar]

    style A fill:#e3f2fd
    style B fill:#e1f5fe
    style C fill:#e0f7fa
    style D fill:#fff9c4
    style E fill:#f3e5f5
    style F fill:#f3e5f5
    style G fill:#e8f5e9
    style H fill:#fce4ec
    style I fill:#fff3e0
    style J fill:#c8e6c9
```

### Paso a Paso Detallado

#### Paso 1: Crear Archivos del Módulo

```bash
# Crear directorio
mkdir -p appweb/slm.framework/modules/ventas

# Crear archivos necesarios
cd appweb/slm.framework/modules/ventas
touch view.lista.slm ajax.lista.slm load.lista.slm end.lista.slm slm_api.slm
```

#### Paso 2: Registrar Módulo en Elasticsearch

```bash
POST /slm-app-dev/_doc/

{
  "name": "Lista de Ventas",
  "directory": "ventas",
  "file": "lista",
  "folder": "ventas.lista",
  "description": "Listado y gestión de ventas",
  "status": "active",
  "slm": {
    "type": "module",
    "timestamp": 1737582700000
  }
}
```

**Guarda el `_id` retornado:** `MODULE_VENTAS_ID`

#### Paso 3: Crear Elemento de Menú

##### Opción A: Menú Simple

```bash
POST /slm-app-dev/_doc/

{
  "name": "Ventas",
  "icon": "fe fe-shopping-cart",
  "folder": "MODULE_VENTAS_ID",
  "display": "true",
  "status": "active",
  "slm": {
    "type": "simple_menu",
    "timestamp": 1737582800000
  }
}
```

**Guarda el `_id` retornado:** `MENU_VENTAS_ID`

##### Opción B: Menú Desplegable (si hay múltiples módulos)

```bash
POST /slm-app-dev/_doc/

{
  "name": "Ventas",
  "icon": "fe fe-shopping-cart",
  "display": "true",
  "module": [
    {
      "folder": "MODULE_VENTAS_LISTA_ID",
      "display": ["true"]
    },
    {
      "folder": "MODULE_VENTAS_REPORTES_ID",
      "display": ["true"]
    }
  ],
  "status": "active",
  "slm": {
    "type": "dropdown_menu",
    "timestamp": 1737582900000
  }
}
```

**Guarda el `_id` retornado:** `MENU_VENTAS_DROPDOWN_ID`

#### Paso 4: Agregar a Grupo de Menú

##### Opción A: Actualizar Grupo Existente

```bash
# Primero, obtener el grupo actual
GET /slm-app-dev/_doc/GROUP_GESTION_ID

# Luego, actualizar agregando el nuevo menú
POST /slm-app-dev/_update/GROUP_GESTION_ID

{
  "script": {
    "source": "ctx._source.menu.add(params.newMenu)",
    "params": {
      "newMenu": {
        "item": "MENU_VENTAS_ID"
      }
    }
  }
}
```

##### Opción B: Crear Nuevo Grupo

```bash
POST /slm-app-dev/_doc/

{
  "title": "Gestión Comercial",
  "order": 30,
  "menu": [
    {
      "item": "MENU_VENTAS_ID"
    }
  ],
  "status": "active",
  "display": "true",
  "slm": {
    "type": "group_menu",
    "timestamp": 1737583000000
  }
}
```

**Guarda el `_id` retornado:** `GROUP_COMERCIAL_ID`

#### Paso 5: Configurar Perfil con ACL

```bash
# Actualizar perfil existente
POST /slm-app-dev/_update/PROFILE_ID

{
  "script": {
    "source": "if (ctx._source.acl.containsKey(params.groupId)) { ctx._source.acl[params.groupId][params.moduleFolder] = params.permissions } else { ctx._source.acl[params.groupId] = [:]; ctx._source.acl[params.groupId][params.moduleFolder] = params.permissions }",
    "params": {
      "groupId": "GROUP_COMERCIAL_ID",
      "moduleFolder": "ventas.lista",
      "permissions": ["read", "create", "update", "delete"]
    }
  }
}
```

O actualizar manualmente el documento completo:

```bash
POST /slm-app-dev/_update/PROFILE_ID

{
  "doc": {
    "acl": {
      "GROUP_INICIO_ID": {
        "home.main": ["read"]
      },
      "GROUP_COMERCIAL_ID": {
        "ventas.lista": ["read", "create", "update", "delete"]
      }
    }
  }
}
```

#### Paso 6: Usuario Ve el Módulo

Cuando el usuario inicia sesión:

1. El sistema carga su perfil y ACL
2. Las funciones `fetchGroupMenus()` y `filterMenuWithPermissions()` en `system/slm_api.slm` construyen y filtran el menú según permisos
3. Se construye el sidebar dinámicamente
4. Solo se muestran grupos, menús y módulos permitidos

---

### Diagrama de Verificación de Acceso

```mermaid
sequenceDiagram
    participant U as Usuario
    participant S as Sistema
    participant ES as Elasticsearch
    participant ACL as Control de Acceso

    U->>S: Login exitoso
    S->>ES: Obtener usuario por email
    ES-->>S: Datos del usuario
    S->>ES: Obtener perfil (profile ID)
    ES-->>S: Perfil con ACL
    S->>ACL: Construir menú según ACL

    loop Por cada grupo de menú
        ACL->>ACL: ¿Usuario tiene acceso al grupo?
        alt Tiene acceso
            loop Por cada menú en el grupo
                ACL->>ACL: ¿Usuario tiene acceso al menú?
                alt Tiene acceso
                    ACL->>S: Incluir menú en sidebar
                else Sin acceso
                    ACL->>S: Omitir menú
                end
            end
        else Sin acceso
            ACL->>S: Omitir grupo completo
        end
    end

    S-->>U: Mostrar sidebar personalizado
```

---

## 📚 Ejemplos Prácticos

### Ejemplo Completo: Sistema de Clientes

Vamos a crear un módulo de gestión de clientes desde cero, incluyendo todos los pasos.

#### 1. Crear Archivos del Módulo

**`view.lista.slm`**
```php
<?php
/**
 * Vista de lista de clientes
 */
?>
<div class="app-content">
    <div class="side-app">
        <div class="page-header">
            <div>
                <h1 class="page-title">Clientes</h1>
                <ol class="breadcrumb">
                    <li class="breadcrumb-item"><a href="#"><?= APP_NAME ?></a></li>
                    <li class="breadcrumb-item active">Clientes</li>
                </ol>
            </div>
            <div class="ms-auto pageheader-btn">
                <?php if (in_array('create', $permissions)): ?>
                <button class="btn btn-primary" onclick="newCliente()">
                    <i class="fe fe-plus"></i> Nuevo Cliente
                </button>
                <?php endif; ?>
            </div>
        </div>

        <div class="row">
            <div class="col-lg-12">
                <div class="card">
                    <div class="card-header">
                        <h3 class="card-title">Lista de Clientes</h3>
                    </div>
                    <div class="card-body">
                        <div class="table-responsive">
                            <table id="clientesTable" class="table table-bordered w-100">
                                <thead>
                                    <tr>
                                        <th>Nombre</th>
                                        <th>Email</th>
                                        <th>Teléfono</th>
                                        <th>Empresa</th>
                                        <th>Estado</th>
                                        <th>Acciones</th>
                                    </tr>
                                </thead>
                            </table>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
```

**`load.lista.slm`**
```php
<?php
/**
 * Carga de datos para vista de clientes
 */

// Obtener permisos del usuario
$permissions = _SLM::getSecurityAccess('clientes.lista');

// Verificar que tenga al menos permiso de lectura
if (empty($permissions) || !in_array('read', $permissions)) {
    header('Location: ./?v=home.main');
    exit;
}

// Variables disponibles en la vista
$canCreate = in_array('create', $permissions);
$canUpdate = in_array('update', $permissions);
$canDelete = in_array('delete', $permissions);
```

**`ajax.lista.slm`**
```php
<?php
/**
 * AJAX handlers para clientes
 */

// Dispatcher
if (isset($_POST['acc']) && function_exists($_POST['acc'])) {
    $_POST['acc']();
}

/**
 * Obtiene lista de clientes
 */
function getClientes(): void
{
    $es_index = getEsIndex();

    $searchTerm = $_POST['search']['value'] ?? '';
    $start = (int)($_POST['start'] ?? 0);
    $length = (int)($_POST['length'] ?? 10);
    $draw = (int)($_POST['draw'] ?? 1);

    $search = ['(slm.type:cliente)AND(status:active)'];
    $fields = ['name.keyword', 'email.keyword', 'company'];

    $params = [
        'from' => $start,
        'size' => $length,
        'sort' => 'name.keyword',
        'order' => 'asc'
    ];

    try {
        $ssp = new SSP($searchTerm, $search, $fields, $params);
        $result = $ssp->setEsIndex($es_index)->setReturn(true)->run();

        jsonSuccess('', [
            'draw' => $draw,
            'recordsTotal' => $result['recordsTotal'],
            'recordsFiltered' => $result['recordsFiltered'],
            'data' => $result['data']
        ]);
    } catch (Exception $e) {
        jsonError($e->getMessage(), [
            'draw' => $draw,
            'recordsTotal' => 0,
            'recordsFiltered' => 0,
            'data' => []
        ]);
    }
}

/**
 * Crea un nuevo cliente
 */
function createCliente(): void
{
    $response = _response(true);
    $logType = 'error';
    $logData = [];

    try {
        // Verificar permisos
        $permissions = _SLM::getSecurityAccess('clientes.lista');
        if (!in_array('create', $permissions)) {
            throw new Exception('No tiene permisos para crear clientes');
        }

        // Validar campos
        validateRequiredFields(['name', 'email']);

        // Validar email
        $email = validateEmail(getPostString('email'));

        // Verificar que el email no exista
        $existing = getObjectWithValue('cliente', 'email.keyword', $email);
        if ($existing) {
            throw new Exception('Ya existe un cliente con ese email');
        }

        // Preparar documento
        $document = [
            'name' => getPostString('name'),
            'email' => $email,
            'phone' => getPostString('phone', ''),
            'company' => getPostString('company', ''),
            'address' => getPostString('address', ''),
            'notes' => getPostString('notes', ''),
            'status' => 'active',
            'slm' => [
                'type' => 'cliente',
                'timestamp' => getTimestamp()
            ]
        ];

        // Crear en Elasticsearch
        $result = setNewObject($document);

        if (isset($result['error']) || ($result['result'] ?? '') !== 'created') {
            throw new Exception('Error al crear el cliente');
        }

        $response = _response(false, 'Cliente creado exitosamente', [
            'id' => $result['_id'],
            'reload' => true
        ]);
        $logType = 'success';

    } catch (Exception $e) {
        $response['message'] = $e->getMessage();
    } finally {
        $logData['response'] = $response;
        addToLog($logType, 'clientes', __FUNCTION__, $logData);
    }

    jsonResponse($response);
}
```

**`end.lista.slm`**
```html
<script>
SLM.module = './?a=clientes.lista';

let clientesTable;

$(document).ready(function() {
    clientesTable = $('#clientesTable').DataTable({
        processing: true,
        serverSide: true,
        ajax: {
            url: SLM.module,
            type: 'POST',
            data: function(d) {
                d.acc = 'getClientes';
            }
        },
        columns: [
            { data: 'name' },
            { data: 'email' },
            { data: 'phone' },
            { data: 'company' },
            {
                data: 'status',
                render: function(data) {
                    return data === 'active'
                        ? '<span class="badge bg-success">Activo</span>'
                        : '<span class="badge bg-danger">Inactivo</span>';
                }
            },
            {
                data: null,
                orderable: false,
                render: function(data, type, row) {
                    return `
                        <button class="btn btn-sm btn-info" onclick="viewCliente('${row.id}')">
                            <i data-feather="eye"></i>
                        </button>
                        <?php if ($canUpdate): ?>
                        <button class="btn btn-sm btn-primary" onclick="editCliente('${row.id}')">
                            <i data-feather="edit"></i>
                        </button>
                        <?php endif; ?>
                        <?php if ($canDelete): ?>
                        <button class="btn btn-sm btn-danger" onclick="deleteCliente('${row.id}')">
                            <i data-feather="trash-2"></i>
                        </button>
                        <?php endif; ?>
                    `;
                }
            }
        ],
        language: {
            url: '//cdn.datatables.net/plug-ins/1.13.4/i18n/es-ES.json'
        },
        drawCallback: function() {
            feather.replace();
        }
    });
});

function newCliente() {
    Swal.fire({
        title: 'Nuevo Cliente',
        html: `
            <form id="clienteForm" class="text-start">
                <div class="mb-3">
                    <label class="form-label">Nombre *</label>
                    <input type="text" class="form-control" name="name" required>
                </div>
                <div class="mb-3">
                    <label class="form-label">Email *</label>
                    <input type="email" class="form-control" name="email" required>
                </div>
                <div class="mb-3">
                    <label class="form-label">Teléfono</label>
                    <input type="tel" class="form-control" name="phone">
                </div>
                <div class="mb-3">
                    <label class="form-label">Empresa</label>
                    <input type="text" class="form-control" name="company">
                </div>
            </form>
        `,
        showCancelButton: true,
        confirmButtonText: 'Guardar',
        cancelButtonText: 'Cancelar',
        preConfirm: () => {
            const formData = new FormData(document.getElementById('clienteForm'));
            const data = {};
            formData.forEach((value, key) => data[key] = value);
            return data;
        }
    }).then(async (result) => {
        if (result.isConfirmed) {
            try {
                SLM.loading('Guardando cliente...');
                const response = await SLM.ajax('createCliente', result.value);
                SLM.closeLoading();

                SLM.success(response.message);
                if (response.reload) clientesTable.ajax.reload();
            } catch (error) {
                SLM.closeLoading();
                SLM.error(error.message);
            }
        }
    });
}
</script>
```

#### 2. Registrar Módulo

```bash
POST /slm-app-dev/_doc/

{
  "name": "Gestión de Clientes",
  "directory": "clientes",
  "file": "lista",
  "folder": "clientes.lista",
  "description": "Listado y administración de clientes",
  "status": "active",
  "slm": {
    "type": "module",
    "timestamp": 1737583100000
  }
}
```

**Resultado:** `_id: "MOD_CLIENTES_123"`

#### 3. Crear Menú Simple

```bash
POST /slm-app-dev/_doc/

{
  "name": "Clientes",
  "icon": "fe fe-users",
  "folder": "MOD_CLIENTES_123",
  "display": "true",
  "status": "active",
  "slm": {
    "type": "simple_menu",
    "timestamp": 1737583200000
  }
}
```

**Resultado:** `_id: "MENU_CLIENTES_456"`

#### 4. Crear/Actualizar Grupo de Menú

```bash
# Crear nuevo grupo "CRM"
POST /slm-app-dev/_doc/

{
  "title": "CRM",
  "order": 20,
  "menu": [
    {
      "item": "MENU_CLIENTES_456"
    }
  ],
  "status": "active",
  "display": "true",
  "slm": {
    "type": "group_menu",
    "timestamp": 1737583300000
  }
}
```

**Resultado:** `_id: "GROUP_CRM_789"`

#### 5. Configurar Perfil

```bash
# Actualizar perfil de "Gerente de Ventas"
POST /slm-app-dev/_update/PROFILE_VENTAS_001

{
  "doc": {
    "acl": {
      "GROUP_INICIO_ID": {
        "home.main": ["read"]
      },
      "GROUP_CRM_789": {
        "clientes.lista": ["read", "create", "update", "delete"]
      }
    }
  }
}
```

#### 6. Resultado Final

El usuario con perfil "Gerente de Ventas" verá:

```
📁 Inicio
  └─ 🏠 Home

📁 CRM
  └─ 👥 Clientes

📁 Configuración
  └─ ... (si tiene permisos)
```

---

## ✅ Mejores Prácticas

### 1. Estructura de Módulos

**✅ Recomendado:**
```
modules/
├── inventario/
│   ├── slm_api.slm                # Funciones compartidas
│   ├── view.productos.slm
│   ├── ajax.productos.slm
│   ├── load.productos.slm
│   ├── end.productos.slm
│   ├── view.categorias.slm
│   └── ajax.categorias.slm
└── clientes/
    ├── slm_api.slm
    ├── view.lista.slm
    └── ajax.lista.slm
```

**❌ Evitar:**
```
modules/
├── producto_listado/               # Nombres muy específicos
├── producto_editar/
└── producto_eliminar/
```

### 2. Nomenclatura de Módulos

**✅ Convención:**
- `folder`: `directorio.archivo` (ej: `inventario.productos`)
- `directory`: Categoría general (ej: `inventario`)
- `file`: Acción específica (ej: `productos`, `categorias`)

**❌ Evitar:**
- Espacios: `"inventario productos"`
- Caracteres especiales: `"inventario-productos"`
- CamelCase: `"inventarioProductos"`

### 3. Verificación de Permisos

**✅ Siempre verificar en AJAX:**
```php
function createProducto(): void
{
    // PRIMERO: Verificar permisos
    $permissions = _SLM::getSecurityAccess('inventario.productos');
    if (!in_array('create', $permissions)) {
        jsonError('Sin permisos');
    }

    // LUEGO: Lógica de negocio
    // ...
}
```

**❌ NUNCA confiar solo en la UI:**
```php
// ❌ MAL: Sin verificación de permisos
function createProducto(): void
{
    // Directamente procede sin verificar
    $result = setNewObject($data);
}
```

### 4. Organización del ACL

**✅ Agrupar por funcionalidad:**
```json
{
  "acl": {
    "GROUP_GESTION_ID": {
      "inventario.productos": ["read", "create", "update"],
      "inventario.categorias": ["read"]
    },
    "GROUP_CONFIG_ID": {
      "config.users": ["read"]
    }
  }
}
```

**❌ No agrupar correctamente:**
```json
{
  "acl": {
    "RANDOM_ID_1": {
      "inventario.productos": ["read"]
    },
    "RANDOM_ID_2": {
      "inventario.categorias": ["read"]
    }
  }
}
```

### 5. Nombres de Menús

**✅ Nombres descriptivos y cortos:**
- ✅ "Productos"
- ✅ "Clientes"
- ✅ "Reportes de Ventas"

**❌ Nombres largos o vagos:**
- ❌ "Lista de todos los productos disponibles"
- ❌ "Item 1"
- ❌ "Módulo de gestión"

### 6. Estados y Status

**✅ Usar `status` consistentemente:**
- `"active"`: Elemento activo y funcional
- `"inactive"`: Elemento desactivado temporalmente

**❌ Mezclar convenciones:**
- ❌ `"enabled"`, `"disabled"`
- ❌ `"visible"`, `"hidden"`
- ❌ `"true"`, `"false"` para status

### 7. Logs y Auditoría

**✅ Usar `addToLog()` en operaciones críticas:**
```php
function deleteProducto(): void
{
    $logType = 'error';
    $logData = ['producto_id' => $_POST['id']];

    try {
        // Lógica...
        $logType = 'success';
    } catch (Exception $e) {
        $logData['error'] = $e->getMessage();
    } finally {
        addToLog($logType, 'inventario', __FUNCTION__, $logData);
    }
}
```

### 8. Validaciones

**✅ Validar siempre en el backend:**
```php
// Validar campos requeridos
validateRequiredFields(['name', 'email', 'price']);

// Validar email
$email = validateEmail($email);

// Validar duplicados
$existing = getObjectWithValue('producto', 'sku.keyword', $sku);
if ($existing) {
    throw new Exception('SKU duplicado');
}
```

**❌ Confiar solo en validaciones del frontend:**
```php
// ❌ MAL: Sin validaciones
$result = setNewObject($_POST);
```

### 9. Manejo de Errores

**✅ Try-catch consistente:**
```php
function myFunction(): void
{
    $response = _response(true);

    try {
        // Lógica...
        $response = _response(false, 'Éxito');
    } catch (InvalidArgumentException $e) {
        $response['message'] = 'Datos inválidos: ' . $e->getMessage();
    } catch (Exception $e) {
        $response['message'] = $e->getMessage();
    }

    jsonResponse($response);
}
```

### 10. Default View

**✅ Establecer vista por defecto en perfiles:**
```json
{
  "default_view": "home.main",
  "acl": {
    "GROUP_ID": {
      "home.main": ["read"]
    }
  }
}
```

**⚠️ Asegurar que el usuario tenga permiso de lectura en su default_view:**

---

## 🔧 Troubleshooting

### Problema 1: El módulo no aparece en el sidebar

#### Síntomas
- El módulo está registrado en Elasticsearch
- El usuario ha iniciado sesión
- No ve el módulo en el sidebar

#### Diagnóstico
```php
// En system/slm_api.slm, agregar debug temporal en fetchGroupMenus() y filterMenuWithPermissions()
function fetchGroupMenus(): array
{
    error_log("fetchGroupMenus called");
    // ... resto del código
}

function filterMenuWithPermissions(array $groups, array $acl = null): array
{
    error_log("filterMenuWithPermissions called");
    error_log("ACL: " . json_encode($acl));
    // ... resto del código
}
```

#### Soluciones

1. **Verificar que el módulo esté en un grupo de menú:**
```bash
# Buscar si el módulo está referenciado en algún menú
GET /slm-app-dev/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "folder": "MODULE_ID"
          }
        },
        {
          "nested": {
            "path": "module",
            "query": {
              "term": {
                "module.folder": "MODULE_ID"
              }
            }
          }
        }
      ]
    }
  }
}
```

2. **Verificar que el grupo esté en el perfil:**
```bash
# Obtener perfil del usuario
GET /slm-app-dev/_doc/PROFILE_ID

# Verificar que el ACL contenga el group_menu_id correcto
```

3. **Verificar el perfil del usuario:**
```bash
# Obtener usuario
GET /slm-app-dev/_doc/USER_ID

# Verificar que profile apunte al perfil correcto
```

4. **Verificar permisos en el ACL:**
```json
{
  "acl": {
    "GROUP_MENU_ID": {
      "modulo.accion": ["read"]  // Debe tener al menos "read"
    }
  }
}
```

---

### Problema 2: Error "Permiso denegado" al ejecutar acción

#### Síntomas
- El usuario puede ver el módulo
- Al intentar crear/actualizar/eliminar, recibe error de permisos

#### Diagnóstico
```php
// En load.accion.slm, verificar permisos
$permissions = _SLM::getSecurityAccess('modulo.accion');
error_log("Permissions for user: " . json_encode($permissions));
```

#### Soluciones

1. **Verificar que el perfil tenga el permiso necesario:**
```json
{
  "acl": {
    "GROUP_ID": {
      "modulo.accion": ["read", "create", "update", "delete"]
    }
  }
}
```

2. **Verificar que la validación en AJAX sea correcta:**
```php
// ✅ Correcto
$permissions = _SLM::getSecurityAccess('inventario.productos');
if (!in_array('create', $permissions)) {
    jsonError('Sin permisos');
}

// ❌ Incorrecto
$permissions = _SLM::getSecurityAccess('inventario.productos');
if (empty($permissions)) {  // Muy genérico
    jsonError('Sin permisos');
}
```

---

### Problema 3: El sidebar muestra módulos duplicados

#### Síntomas
- Un mismo módulo aparece múltiples veces en el sidebar
- Pueden estar en diferentes grupos

#### Diagnóstico
```bash
# Buscar todos los menús que apuntan al mismo módulo
GET /slm-app-dev/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "folder": "MODULE_ID"
          }
        },
        {
          "nested": {
            "path": "module",
            "query": {
              "term": {
                "module.folder": "MODULE_ID"
              }
            }
          }
        }
      ]
    }
  }
}
```

#### Soluciones

1. **Eliminar menús duplicados:**
```bash
DELETE /slm-app-dev/_doc/DUPLICATE_MENU_ID
```

2. **Verificar que el módulo no esté en múltiples grupos sin intención:**
```bash
# Buscar grupos que contengan el menú
GET /slm-app-dev/_search
{
  "query": {
    "nested": {
      "path": "menu",
      "query": {
        "term": {
          "menu.item": "MENU_ID"
        }
      }
    }
  }
}
```

---

### Problema 4: Usuario no puede acceder a su default_view

#### Síntomas
- Usuario hace login exitosamente
- Es redirigido a página de error o home incorrectamente

#### Diagnóstico
```php
// Verificar en _SLM.slm, método loadSession()
error_log("User profile: " . json_encode(_SLM::$session['user']['profile'] ?? null));
error_log("Default view: " . (_SLM::$session['user']['profile']['default_view'] ?? 'none'));
```

#### Soluciones

1. **Verificar que default_view esté configurado:**
```json
{
  "_id": "PROFILE_ID",
  "name": "Mi Perfil",
  "default_view": "home.main",  // ✅ Debe existir
  "acl": {
    // ...
  }
}
```

2. **Verificar que el usuario tenga permiso de lectura en el default_view:**
```json
{
  "default_view": "inventario.productos",
  "acl": {
    "GROUP_ID": {
      "inventario.productos": ["read"]  // ✅ Debe tener "read"
    }
  }
}
```

3. **Fallback si default_view no es accesible:**
```php
// En el código de inicio de sesión
$defaultView = $user['profile']['default_view'] ?? 'home.main';
$permissions = _SLM::getSecurityAccess($defaultView, $acl);

if (empty($permissions) || !in_array('read', $permissions)) {
    // Buscar primer módulo con permisos
    $groups = fetchGroupMenus();
    $menu = filterMenuWithPermissions($groups, $acl);
    foreach ($menu as $group) {
        if (!empty($group['modules'])) {
            $defaultView = $group['modules'][0]['folder'];
            break;
        }
    }
}

header("Location: ./?v=$defaultView");
```

---

### Problema 5: Módulo registrado pero no detectado por generate_acl()

#### Síntomas
- Archivos físicos existen en `modules/directorio/`
- El módulo no aparece en la interfaz de configuración

#### Diagnóstico
```php
// En modules/config/ajax.modules.slm
$fileModules = generate_acl('../slm.framework/modules');
error_log("Detected modules: " . json_encode($fileModules));
```

#### Soluciones

1. **Verificar nombre de archivos:**
```bash
# ✅ Correcto
view.lista.slm
ajax.lista.slm
load.lista.slm
end.lista.slm

# ❌ Incorrecto (falta prefijo)
lista.slm
listado.php
view_lista.slm
```

2. **Verificar que los archivos tengan prefijos válidos:**
- `proc.*`
- `load.*`
- `view.*`
- `ajax.*`
- `end.*`

3. **Verificar permisos de archivos:**
```bash
ls -la appweb/slm.framework/modules/directorio/
# Deben ser legibles por el servidor web
```

---

### Problema 6: ACL no se aplica correctamente

#### Síntomas
- Usuario tiene permisos en el perfil
- Aún así no puede acceder al módulo

#### Diagnóstico
```php
// En _SLM.slm, método getSecurityAccess()
public static function getSecurityAccess(string $view, array $acl = null): array
{
    error_log("Getting security for view: $view");
    error_log("ACL: " . json_encode($acl));

    if (!is_array($acl)) {
        $acl = self::getUserACL();
        error_log("User ACL: " . json_encode($acl));
    }

    $key = self::findKey($acl, $view);
    error_log("Found key: " . ($key ?? 'null'));

    if (empty($key)) {
        return [];
    }

    return $acl[$key][$view];
}
```

#### Soluciones

1. **Verificar estructura del ACL:**
```json
{
  "acl": {
    "GROUP_MENU_ID": {                    // ✅ Clave: ID del group_menu
      "modulo.accion": ["read", "create"] // ✅ Valor: folder y permisos
    }
  }
}
```

2. **NO usar esta estructura:**
```json
{
  "acl": {
    "modulo.accion": ["read", "create"]   // ❌ Falta el group_menu_id
  }
}
```

3. **Verificar que el folder sea exacto:**
```json
// ✅ Correcto
"inventario.productos": ["read"]

// ❌ Incorrecto
"inventario-productos": ["read"]  // Usa guión en lugar de punto
"Inventario.Productos": ["read"]  // Mayúsculas incorrectas
```

---

### Problema 7: DataTable no carga datos

#### Síntomas
- La tabla se muestra vacía
- No hay errores en consola
- El spinner de carga se queda indefinidamente

#### Diagnóstico
```javascript
// En el DataTable, agregar error handler
ajax: {
    url: SLM.module,
    type: 'POST',
    data: function(d) {
        d.acc = 'getProductos';
    },
    error: function(xhr, error, code) {
        console.error('DataTable error:', error, code);
        console.error('Response:', xhr.responseText);
    }
}
```

#### Soluciones

1. **Verificar que el AJAX handler retorne el formato correcto:**
```php
// ✅ Correcto
jsonSuccess('', [
    'draw' => $draw,
    'recordsTotal' => 100,
    'recordsFiltered' => 100,
    'data' => $records
]);

// ❌ Incorrecto
echo json_encode($records);  // Falta formato DataTables
```

2. **Verificar que la función AJAX exista:**
```php
// En ajax.accion.slm
if (isset($_POST['acc'])) {
    $function = $_POST['acc'];
    if (!function_exists($function)) {
        jsonError('Acción no válida');  // ✅ Esto ayuda al debug
    }
    $function();
}
```

3. **Verificar permisos en el handler:**
```php
function getProductos(): void
{
    // Verificar que el usuario tenga permiso de lectura
    $permissions = _SLM::getSecurityAccess('inventario.productos');
    if (!in_array('read', $permissions)) {
        jsonError('Sin permisos de lectura');
    }

    // ... resto del código
}
```

---

### Problema 8: Feather Icons no se muestran

#### Síntomas
- Los iconos aparecen como `[?]` o no se ven
- Se ven etiquetas `<i data-feather="..."></i>` sin renderizar

#### Soluciones

1. **Llamar `feather.replace()` después de cargar contenido dinámico:**
```javascript
// ✅ Después de DataTable render
drawCallback: function() {
    feather.replace();
}

// ✅ Después de cargar contenido con AJAX
$('#container').html(newContent);
feather.replace();
```

2. **Verificar que Feather Icons esté cargado:**
```html
<!-- En _header.slm -->
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
```

---

### Herramientas de Debug Útiles

#### 1. Ver ACL del Usuario Actual

```php
// Agregar temporalmente en cualquier vista
<?php
echo '<pre>';
print_r(_SLM::getUserACL());
echo '</pre>';
?>
```

#### 2. Ver Menú Generado

```php
// En cualquier vista
<?php
echo '<pre>';
$groups = fetchGroupMenus();
$menu = filterMenuWithPermissions($groups);  // Filtra con permisos del usuario actual
print_r($menu);
echo '</pre>';
?>
```

#### 3. Ver Permisos para Módulo Específico

```php
<?php
$permissions = _SLM::getSecurityAccess('inventario.productos');
echo '<pre>';
print_r($permissions);
echo '</pre>';
?>
```

#### 4. Ver Sesión Completa

```php
<?php
echo '<pre>';
print_r(_SLM::$session);
echo '</pre>';
?>
```

#### 5. Elasticsearch Query para Ver Todo el Menú

```bash
# Ver todos los grupos de menú
GET /slm-app-dev/_search
{
  "query": {
    "term": {
      "slm.type": "group_menu"
    }
  },
  "sort": [
    {
      "order": {
        "order": "asc"
      }
    }
  ]
}

# Ver todos los módulos
GET /slm-app-dev/_search
{
  "query": {
    "term": {
      "slm.type": "module"
    }
  },
  "sort": [
    {
      "name.keyword": {
        "order": "asc"
      }
    }
  ]
}

# Ver todos los perfiles
GET /slm-app-dev/_search
{
  "query": {
    "term": {
      "slm.type": "profile"
    }
  }
}
```

---

## 📝 Checklist de Implementación

Use este checklist al crear un nuevo módulo:

- [ ] **Paso 1:** Crear estructura de archivos
  - [ ] `view.accion.slm` creado
  - [ ] `ajax.accion.slm` creado (si necesita AJAX)
  - [ ] `load.accion.slm` creado (si necesita datos)
  - [ ] `end.accion.slm` creado (si necesita JS)
  - [ ] `slm_api.slm` creado (si tiene funciones compartidas)

- [ ] **Paso 2:** Registrar módulo en Elasticsearch
  - [ ] Documento `module` creado
  - [ ] Campo `folder` correcto (`directorio.archivo`)
  - [ ] `_id` del módulo guardado

- [ ] **Paso 3:** Crear elemento de menú
  - [ ] Elegir tipo: simple_menu o dropdown_menu
  - [ ] Documento creado con referencia al módulo
  - [ ] `_id` del menú guardado

- [ ] **Paso 4:** Agregar a grupo de menú
  - [ ] Grupo existente actualizado o nuevo grupo creado
  - [ ] Menú agregado al array `menu[]`
  - [ ] `_id` del grupo guardado

- [ ] **Paso 5:** Configurar permisos
  - [ ] Perfil actualizado con ACL
  - [ ] ACL incluye `group_menu_id` correcto
  - [ ] Permisos adecuados asignados (`read`, `create`, `update`, `delete`)

- [ ] **Paso 6:** Verificar en UI
  - [ ] Usuario con perfil puede ver el módulo en sidebar
  - [ ] Usuario puede acceder al módulo
  - [ ] Permisos se aplican correctamente

- [ ] **Paso 7:** Probar funcionalidades
  - [ ] Lectura funciona
  - [ ] Creación funciona (si tiene permiso)
  - [ ] Actualización funciona (si tiene permiso)
  - [ ] Eliminación funciona (si tiene permiso)

- [ ] **Paso 8:** Logs y auditoría
  - [ ] `addToLog()` implementado en operaciones críticas
  - [ ] Logs se guardan correctamente en Elasticsearch

---

## 🎓 Conclusión

Este documento proporciona una guía completa para trabajar con el sistema de módulos, menús y permisos del SLM Framework.

### Puntos Clave a Recordar:

1. **Todo es dinámico**: Módulos, menús y permisos se gestionan en Elasticsearch
2. **Jerarquía clara**: Módulos → Menús → Grupos → Perfiles → Usuarios
3. **Seguridad primero**: Siempre verificar permisos en backend
4. **Logs son importantes**: Usar `addToLog()` en operaciones críticas
5. **Convenciones consistentes**: Seguir nomenclatura establecida

### Recursos Adicionales

- **Documentación de Elasticsearch**: Para consultas avanzadas
- **SLM Framework Standards**: Para convenciones de código
- **API Reference**: Para funciones disponibles del framework

### Soporte

Para preguntas o problemas:
1. Consultar la sección de [Troubleshooting](#-troubleshooting)
2. Revisar los logs en Elasticsearch
3. Contactar al equipo de desarrollo principal

---

**Happy Coding! 🚀**

_Documento mantenido por el equipo de desarrollo de SLM Framework_
