# Guía de Desarrollo de Módulos

Esta guía explica cómo crear módulos en el SLM Framework.

## Estructura de un Módulo

Un módulo es una colección de archivos `.slm` en un directorio bajo `modules/`:

```
modules/
└── mimodulo/
    ├── slm_api.slm        # Funciones específicas del módulo (opcional)
    ├── proc.action.slm    # Pre-procesamiento (opcional)
    ├── load.action.slm    # Carga de datos (opcional)
    ├── view.action.slm    # Vista HTML (requerido)
    ├── ajax.action.slm    # Handlers AJAX (opcional)
    └── end.action.slm     # JavaScript (opcional)
```

## Orden de Carga de Archivos

Para una solicitud de vista (`?v=mimodulo.action`):

1. `slm_api.slm` - Funciones del módulo
2. `proc.action.slm` - Pre-procesamiento
3. `load.action.slm` - Carga de datos
4. `_header.slm` - Encabezado de página
5. `view.action.slm` - Contenido principal
6. `_footer.slm` - Pie de página (incluye `end.action.slm`)

Para una solicitud AJAX (`?a=mimodulo.action`):

1. `slm_api.slm` - Funciones del módulo
2. `ajax.action.slm` - Procesa solicitud y retorna JSON

## Creando un Módulo Básico

### Paso 1: Crear Directorio

```bash
mkdir -p appweb/slm.framework/modules/productos
```

### Paso 2: Crear Archivo de Vista

`view.list.slm`:

```php
<?php
/**
 * Módulo de Productos - Vista de Lista
 */
?>
<div class="container-fluid">
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h1 class="h3">Productos</h1>
        <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#productModal">
            <i data-feather="plus"></i> Nuevo Producto
        </button>
    </div>

    <div class="card">
        <div class="card-body">
            <table id="productsTable" class="table table-striped">
                <thead>
                    <tr>
                        <th>Nombre</th>
                        <th>Precio</th>
                        <th>Estado</th>
                        <th>Acciones</th>
                    </tr>
                </thead>
            </table>
        </div>
    </div>
</div>

<!-- Modal -->
<div class="modal fade" id="productModal" tabindex="-1">
    <div class="modal-dialog">
        <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="productForm">
                <div class="modal-body">
                    <input type="hidden" id="productId" name="id">
                    <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">Precio</label>
                        <input type="number" class="form-control" name="price" step="0.01">
                    </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: Crear Handler AJAX

`ajax.list.slm`:

```php
<?php
/**
 * Módulo de Productos - Handler AJAX
 */

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

/**
 * Obtener lista de productos para DataTables.
 */
function getProducts(): 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:product)'];
    $fields = ['name.keyword', '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();

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

/**
 * Crear un nuevo producto.
 */
function createProduct(): void
{
    $response = _response(true);
    $log_type = 'error';
    $log_data = [];

    try {
        validateRequiredFields(['name']);

        $tmp = [
            'name' => getPostString('name'),
            'price' => (float)getPostValue('price', 0),
            'status' => 'active',
            'slm' => [
                'type' => 'product',
                'timestamp' => getTimestamp()
            ]
        ];

        $result = setNewObject($tmp);

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

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

    } catch (Exception $e) {
        $response['message'] = $e->getMessage();
    } finally {
        $log_data['return'] = $response;
        addToLog($log_type, 'products', __FUNCTION__, $log_data);
    }

    exit(json_encode($response));
}

/**
 * Actualizar un producto.
 */
function updateProduct(): void
{
    $response = _response(true);
    $log_type = 'error';
    $log_data = [];

    try {
        validateRequiredFields(['id', 'name']);

        $id = getPostString('id');

        $tmp = [
            'name' => getPostString('name'),
            'price' => (float)getPostValue('price', 0),
            'slm' => [
                'type' => 'product',
                'timestamp' => getTimestamp()
            ]
        ];

        $result = updateObjectByID($id, $tmp);

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

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

    } catch (Exception $e) {
        $response['message'] = $e->getMessage();
    } finally {
        $log_data['return'] = $response;
        addToLog($log_type, 'products', __FUNCTION__, $log_data);
    }

    exit(json_encode($response));
}

/**
 * Eliminar un producto (eliminación suave).
 */
function deleteProduct(): void
{
    $response = _response(true);
    $log_type = 'error';
    $log_data = [];

    try {
        $id = getPostString('id');

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

        $result = updateObjectByID($id, [
            'status' => 'inactive',
            'slm' => ['type' => 'product', 'timestamp' => getTimestamp()]
        ]);

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

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

    } catch (Exception $e) {
        $response['message'] = $e->getMessage();
    } finally {
        $log_data['return'] = $response;
        addToLog($log_type, 'products', __FUNCTION__, $log_data);
    }

    exit(json_encode($response));
}
```

### Paso 4: Crear JavaScript

`end.list.slm`:

```html
<script>
SLM.module = './?a=products.list';

let productsTable;
const productModal = new bootstrap.Modal(document.getElementById('productModal'));

// Inicializar DataTable
$(document).ready(function() {
    productsTable = $('#productsTable').DataTable({
        processing: true,
        serverSide: true,
        ajax: {
            url: SLM.module,
            type: 'POST',
            data: function(d) {
                d.acc = 'getProducts';
            }
        },
        columns: [
            { data: 'name' },
            {
                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,
                render: function(data, type, row) {
                    return `
                        <button class="btn btn-sm btn-outline-primary" onclick="editProduct('${row.id}')">
                            <i data-feather="edit" style="width:14px;"></i>
                        </button>
                        <button class="btn btn-sm btn-outline-danger" onclick="deleteProduct('${row.id}')">
                            <i data-feather="trash-2" style="width:14px;"></i>
                        </button>
                    `;
                }
            }
        ],
        drawCallback: function() {
            feather.replace();
        }
    });
});

// Envío de formulario
document.getElementById('productForm').addEventListener('submit', async function(e) {
    e.preventDefault();

    const id = document.getElementById('productId').value;
    const formData = new FormData(this);
    const data = {};
    formData.forEach((value, key) => data[key] = value);

    try {
        const action = id ? 'updateProduct' : 'createProduct';
        const result = await SLM.ajax(action, data);

        SLM.success(result.message);

        if (result.closeModal) productModal.hide();
        if (result.reloadTable) productsTable.ajax.reload();

    } catch (error) {
        SLM.error(error.message);
    }
});

// Editar producto
async function editProduct(id) {
    try {
        SLM.loading();
        const result = await SLM.ajax('getProduct', { id });
        // Llenar formulario...
        SLM.closeLoading();
        productModal.show();
    } catch (error) {
        SLM.error(error.message);
    }
}

// Eliminar producto
async function deleteProduct(id) {
    const result = await SLM.confirm('¿Eliminar este producto?');
    if (!result.isConfirmed) return;

    try {
        const response = await SLM.ajax('deleteProduct', { id });
        SLM.success(response.message);
        if (response.reloadTable) productsTable.ajax.reload();
    } catch (error) {
        SLM.error(error.message);
    }
}
</script>
```

### Paso 5: Agregar al Menú

Edita `system/slm_api.slm`:

```php
function getMenu(bool $getAll = false, $acl = null): array
{
    // ... código existente ...

    if ($getAll || _SLM::getSecurityAccess("products.list", $acl)) {
        $module["folder"] = "products.list";
        $module["name"] = "Productos";
        $module["visible"] = true;
        $section["modules"][] = $module;
        unset($module);
    }

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

### Paso 6: Actualizar ACL

Agrega permisos al perfil del usuario:

```json
{
  "products": {
    "products.list": ["read", "create", "update", "delete"]
  }
}
```

## Mejores Prácticas

### 1. Usar Archivos de Pre-procesamiento

`proc.list.slm` para validaciones o redirecciones:

```php
<?php
// Verificar parámetros requeridos
if (empty($_GET['filter'])) {
    header('Location: ./?v=products.list&filter=all');
    exit;
}
```

### 2. Usar Archivos de Carga para Datos

`load.list.slm` para preparar datos de la vista:

```php
<?php
$permissions = _SLM::getSecurityAccess('products.list');
$canCreate = in_array('create', $permissions);
$canUpdate = in_array('update', $permissions);
$canDelete = in_array('delete', $permissions);

// Cargar categorías para dropdown
$categories = getCategories();
```

### 3. Archivo API del Módulo

`slm_api.slm` para funciones reutilizables:

```php
<?php
/**
 * API del Módulo de Productos
 */

function getCategories(): array
{
    $es_index = getEsIndex();
    $result = json_decode(es_execute(
        "/$es_index/_search?q=slm.type:category&size=100"
    ), true);

    return $result['hits']['hits'] ?? [];
}

function validateProductData(array $data): void
{
    if (empty($data['name'])) {
        throw new InvalidArgumentException('El nombre es requerido');
    }
    if (strlen($data['name']) > 100) {
        throw new InvalidArgumentException('El nombre es muy largo');
    }
}
```

### 4. Verificación de Permisos

Siempre verifica permisos en handlers AJAX:

```php
function deleteProduct(): void
{
    $permissions = _SLM::getSecurityAccess('products.list');
    if (!in_array('delete', $permissions)) {
        exit(json_encode(_response(true, 'Permiso denegado')));
    }

    // ... resto de la función
}
```

## Múltiples Acciones en un Módulo

Puedes tener múltiples acciones por módulo:

```
modules/products/
├── view.list.slm      # ?v=products.list
├── view.detail.slm    # ?v=products.detail
├── view.report.slm    # ?v=products.report
├── ajax.list.slm      # ?a=products.list
├── ajax.detail.slm    # ?a=products.detail
├── load.list.slm
├── load.detail.slm
├── end.list.slm
└── end.detail.slm
```
