# 03 - Desarrollo de Módulos

## Scaffolding con `bin/make`

### Crear módulo completo

```bash
php bin/make module Inventario
```

Genera los siguientes archivos:

| Archivo | Propósito |
|---|---|
| `app/controllers/InventarioController.php` | Controlador con CRUD base |
| `app/models/InventarioModel.php` | Modelo con atributos y tabla |
| `app/views/inventario/list.php` | Vista de listado |
| `app/views/inventario/view.php` | Vista de detalle |
| `database/migrations/{timestamp}_create_inventario_table.php` | Migración de tabla |
| `database/seeders/InventarioSeeder.php` | Seeder de datos |
| `database/seeders/InventarioPermissionsSeeder.php` | Seeder de permisos |
| `app/config/Modules.php` | Se agrega entrada al registro |

### Opciones de scaffolding

```bash
php bin/make module Inventario --basic                  # Solo controller, model, views y seeder
php bin/make module Inventario --no-migration            # Sin migración
php bin/make module Inventario --no-permissions           # Sin seeder de permisos
php bin/make module Inventario --no-module-registry       # Sin auto-registro en Modules.php
php bin/make module Inventario --category="Operaciones"   # Categoría personalizada
php bin/make module Inventario --menu-title="Inventarios" # Título de menú
php bin/make module Inventario --icon='<i class="bi bi-box fs-2"></i>'
```

### Crear seeder individual

```bash
php bin/make seeder EstadosSeeder
```

## Flujo completo de un módulo nuevo

### Paso 1: Generar el scaffolding

```bash
php bin/make module Producto
```

### Paso 2: Definir atributos del modelo

Editar `app/models/ProductoModel.php`:

```php
<?php

class ProductoModel extends Model
{
    protected static $TABLE_NAME = 'productos';
    protected static $VIEW_NAME  = 'productos';
    protected static $LOG   = true;   // activar audit log
    protected static $CACHE = false;

    public static function getOptionsAttributes()
    {
        return array(
            array('Type' => 'AutoincrementId', 'Name' => 'Id'),
            array(
                'Type'     => 'text',
                'Name'     => 'Nombre',
                'Title'    => 'Nombre del Producto',
                'Required' => true,
            ),
            array(
                'Type'     => 'text',
                'Name'     => 'Codigo',
                'Title'    => 'Código',
                'Required' => true,
            ),
            array(
                'Type'  => 'textarea',
                'Name'  => 'Descripcion',
                'Title' => 'Descripción',
            ),
            array(
                'Type'     => 'decimal',
                'Name'     => 'Precio',
                'Title'    => 'Precio Unitario',
                'Required' => true,
            ),
            array(
                'Type'     => 'integer',
                'Name'     => 'Stock',
                'Title'    => 'Cantidad en Stock',
                'Required' => true,
            ),
            array(
                'Type'     => 'select',
                'Name'     => 'CategoriaId',
                'Title'    => 'Categoría',
                'Tabla'    => 'categorias',
                'Required' => true,
            ),
            array(
                'Type'     => 'select',
                'Name'     => 'Estado',
                'Title'    => 'Estado',
                'Required' => true,
                'Tabla'    => 'estados',
            ),
            array('Type' => 'RegistrationDate', 'Name' => 'FechaRegistro'),
            array('Type' => 'RegistrationUser', 'Name' => 'UsuarioRegistro'),
            array('Type' => 'ModificationDate', 'Name' => 'FechaModificacion'),
            array('Type' => 'ModificationUser', 'Name' => 'UsuarioModificacion'),
        );
    }
}
```

### Paso 3: Ajustar la migración

Editar `database/migrations/{timestamp}_create_productos_table.php` para que coincida con los atributos:

```php
public function up(PDO $pdo)
{
    $pdo->exec("
        CREATE TABLE IF NOT EXISTS productos (
            Id INT AUTO_INCREMENT PRIMARY KEY,
            Nombre VARCHAR(100) NOT NULL,
            Codigo VARCHAR(100) NOT NULL,
            Descripcion TEXT NULL,
            Precio DECIMAL(12,2) NOT NULL DEFAULT 0,
            Stock INT NOT NULL DEFAULT 0,
            CategoriaId INT NULL,
            Estado INT NOT NULL DEFAULT 1,
            FechaRegistro DATETIME NULL,
            UsuarioRegistro INT NULL,
            FechaModificacion DATETIME NULL,
            UsuarioModificacion INT NULL,
            TenantId INT NOT NULL DEFAULT 1
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    ");
}

public function down(PDO $pdo)
{
    $pdo->exec("DROP TABLE IF EXISTS productos;");
}
```

### Paso 4: Implementar el controlador

Editar `app/controllers/ProductoController.php`:

```php
<?php

class ProductoController extends Controller
{
    public static $TITLE_NAME  = 'productos';
    public static $MODULE_NAME = 'producto';
    protected $ViewFolder = 'producto';

    protected function loadAccessControl()
    {
        $this->AccessControl = array(
            'create'       => '@',
            'edit'         => '@',
            'remove'       => '@',
            'view'         => '@',
            'list'         => '@',
            'dataListAjax' => '@',
        );
    }

    // ── CREAR ──────────────────────────────────────────────
    public function createAction()
    {
        Menu::setActive('herramientas_producto');
        $this->Model = new ProductoModel();

        if (isset($_POST[get_class($this->Model)])) {
            if ($this->Model->save()) {
                UserFlash::setFlash('Success', 'Producto creado correctamente');
                Router::redirect_to_action($this->Module, 'view', array('Id' => $this->Model->Id->getValue()));
            }
        }

        Controller::generateCsrfToken();
        $parameters = $this->loadMetadata();
        $parameters['model'] = $this->Model;
        View::render_view($this->ViewFolder . '/create', $parameters);
    }

    // ── EDITAR ─────────────────────────────────────────────
    public function editAction()
    {
        Menu::setActive('herramientas_producto');
        $this->Model = new ProductoModel();

        if (isset($_POST[get_class($this->Model)])) {
            $this->Model->loadById($_POST[get_class($this->Model)]['Id']);
            if ($this->Model->save()) {
                UserFlash::setFlash('Success', 'Producto actualizado correctamente');
                Router::redirect_to_action($this->Module, 'view', array('Id' => $this->Model->Id->getValue()));
            }
        }

        if (isset($_GET['Id'])) {
            Controller::generateCsrfToken();
            $this->Model->loadById($_GET['Id']);
            $parameters = $this->loadMetadata();
            $parameters['model'] = $this->Model;
            View::render_view($this->ViewFolder . '/edit', $parameters);
        } else {
            UserFlash::setFlash('Error', 'Parámetros inválidos');
            Router::redirect_to_action($this->Module, 'list');
        }
    }

    // ── VER DETALLE ────────────────────────────────────────
    public function viewAction()
    {
        Menu::setActive('herramientas_producto');
        $this->Model = new ProductoModel();

        if (isset($_GET['Id'])) {
            $this->Model->loadById($_GET['Id']);
            $parameters = $this->loadMetadata();
            $parameters['model'] = $this->Model;
            View::render_view($this->ViewFolder . '/view', $parameters);
        } else {
            UserFlash::setFlash('Error', 'Parámetros inválidos');
            Router::redirect_to_action($this->Module, 'list');
        }
    }

    // ── ELIMINAR ───────────────────────────────────────────
    public function removeAction()
    {
        Menu::setActive('herramientas_producto');

        if (isset($_GET['Id'])) {
            $model = new ProductoModel();
            $model->loadById($_GET['Id']);
            if ($model->deleteFromParameters(array(
                'WHERE' => array(array('name' => 'Id', 'value' => $_GET['Id']))
            ))) {
                UserFlash::setFlash('Success', 'Producto eliminado');
            } else {
                UserFlash::setFlash('Error', 'No se pudo eliminar');
            }
        }

        Router::redirect_to_action($this->Module, 'list');
    }

    // ── LISTA ──────────────────────────────────────────────
    public function listAction()
    {
        Menu::setActive('herramientas_producto');
        $parameters = $this->loadMetadata();
        $table = $this->getListAjaxObject();
        $parameters['listaHtml'] = $table->getHtml();
        View::render_view($this->ViewFolder . '/list', $parameters);
    }

    public function dataListAjaxAction()
    {
        $table = $this->getListAjaxObject();
        Response::json($table->generateDataListAjax());
    }

    protected function getListAjaxObject()
    {
        $table = new ListaAjax($this->Module, $this->CurrentAction);
        $model = new ProductoModel();
        $table->setModel($model);

        $fieldsShow = array('Id', 'Codigo', 'Nombre', 'Precio', 'Stock', 'Estado');
        $titles     = array('ID', 'Código', 'Nombre', 'Precio', 'Stock', 'Estado');

        $table->setData(null, null, $titles, null, $fieldsShow);
        $table->setPermission($this->Permission);
        $table->setState(true);  // mostrar columna de estado

        return $table;
    }
}
```

### Paso 5: Crear las vistas

#### `app/views/producto/create.php`

```php
<div class="post d-flex flex-column-fluid" id="kt_post">
    <div id="kt_content_container" class="container-xxl">
        <?php View::load_view('producto/_form', array(
            'model'          => $model,
            'controllerName' => $controllerName,
            'currentAction'  => $currentAction,
        )); ?>
    </div>
</div>
```

#### `app/views/producto/edit.php`

Idéntico a `create.php` (el partial `_form.php` detecta si es creación o edición).

#### `app/views/producto/_form.php`

```php
<form method="post" action="<?php echo Router::create_action_url($controllerName, $currentAction); ?>"
      enctype="multipart/form-data" class="form">

    <!-- Token CSRF -->
    <input type="hidden" name="_csrf_token" value="<?php echo Controller::generateCsrfToken(); ?>">

    <!-- ID oculto (para edición) -->
    <input type="hidden" name="ProductoModel[Id]" value="<?php echo $model->Id->getValue(); ?>">

    <div class="card mb-5 mb-xl-10">
        <div class="card-header">
            <h3 class="card-title">Datos del Producto</h3>
        </div>
        <div class="card-body">
            <!-- Campo texto -->
            <div class="row mb-6">
                <label class="col-lg-3 col-form-label required fw-bold fs-6">
                    <?php echo $model->Nombre->getTitle(); ?>
                </label>
                <div class="col-lg-9 fv-row">
                    <input type="text" name="ProductoModel[Nombre]"
                           class="form-control form-control-lg form-control-solid"
                           value="<?php echo $model->Nombre->getValue(); ?>"
                           placeholder="Ingrese el nombre" />
                </div>
            </div>

            <!-- Campo select -->
            <div class="row mb-6">
                <label class="col-lg-3 col-form-label required fw-bold fs-6">
                    <?php echo $model->CategoriaId->getTitle(); ?>
                </label>
                <div class="col-lg-9 fv-row">
                    <select name="ProductoModel[CategoriaId]"
                            class="form-select form-select-solid"
                            data-control="select2">
                        <option value="">Seleccione...</option>
                        <?php /* opciones del select cargadas desde el atributo */ ?>
                    </select>
                </div>
            </div>

            <!-- Campo textarea -->
            <div class="row mb-6">
                <label class="col-lg-3 col-form-label fw-bold fs-6">
                    <?php echo $model->Descripcion->getTitle(); ?>
                </label>
                <div class="col-lg-9 fv-row">
                    <textarea name="ProductoModel[Descripcion]"
                              class="form-control form-control-solid"
                              rows="4"><?php echo $model->Descripcion->getValue(); ?></textarea>
                </div>
            </div>
        </div>

        <div class="card-footer d-flex justify-content-end py-6">
            <a href="<?php echo Router::create_action_url($controllerName, 'list'); ?>"
               class="btn btn-light me-3">Cancelar</a>
            <button type="submit" class="btn btn-primary">Guardar</button>
        </div>
    </div>
</form>
```

**Puntos clave de las vistas:**
- Los nombres de campos POST siguen el patrón `NombreModelo[NombreAtributo]`.
- El token CSRF se incluye como hidden input `_csrf_token`.
- Clases CSS: Metronic 8 (`form-control-solid`, `card`, `btn-primary`, etc.).
- Los selects usan `data-control="select2"` para Select2.
- Los parciales compartidos llevan prefijo `_` (ejemplo: `_form.php`, `_headboard.php`).

#### `app/views/producto/list.php`

```php
<div class="post d-flex flex-column-fluid" id="kt_post">
    <div id="kt_content_container" class="container-xxl">
        <div class="card">
            <div class="card-header border-0 pt-6">
                <div class="card-title">
                    <h2>Productos</h2>
                </div>
                <div class="card-toolbar">
                    <a href="<?php echo Router::create_action_url($controllerName, 'create'); ?>"
                       class="btn btn-primary">
                        <i class="bi bi-plus-lg"></i> Agregar
                    </a>
                </div>
            </div>
            <div class="card-body pt-0">
                <?php echo $listaHtml; ?>
            </div>
        </div>
    </div>
</div>
```

#### `app/views/producto/view.php`

```php
<div class="post d-flex flex-column-fluid" id="kt_post">
    <div id="kt_content_container" class="container-xxl">
        <div class="card mb-5 mb-xl-10">
            <div class="card-header">
                <h3 class="card-title">Detalle del Producto</h3>
                <div class="card-toolbar">
                    <a href="<?php echo Router::create_action_url($controllerName, 'edit', array('Id' => $model->Id->getValue())); ?>"
                       class="btn btn-sm btn-warning me-2">Editar</a>
                    <a href="<?php echo Router::create_action_url($controllerName, 'list'); ?>"
                       class="btn btn-sm btn-light">Volver</a>
                </div>
            </div>
            <div class="card-body">
                <div class="row mb-4">
                    <label class="col-lg-3 fw-bold text-muted"><?php echo $model->Nombre->getTitle(); ?></label>
                    <div class="col-lg-9">
                        <span class="fw-bolder fs-6"><?php echo $model->Nombre->getValue(); ?></span>
                    </div>
                </div>
                <!-- Repetir para cada campo... -->
            </div>
        </div>
    </div>
</div>
```

### Paso 6: Ejecutar migración y seeder

```bash
php bin/migrate up
php bin/seed ProductoPermissionsSeeder
```

### Paso 7: Validar

```bash
php -l app/controllers/ProductoController.php
php -l app/models/ProductoModel.php
php bin/doctor
```

## Configuración de ListaAjax

### Acciones de fila personalizadas

```php
$table->setActions(array(
    array(
        'name'       => 'Ver',
        'controller' => $this->Module,
        'action'     => 'view',
        'icon'       => 'eye',
        'color'      => 'primary',
    ),
    array(
        'name'       => 'Editar',
        'controller' => $this->Module,
        'action'     => 'edit',
        'icon'       => 'pencil',
        'color'      => 'warning',
    ),
    array(
        'name'       => 'Eliminar',
        'controller' => $this->Module,
        'action'     => 'remove',
        'icon'       => 'trash',
        'color'      => 'danger',
        'confirm'    => '¿Está seguro de eliminar este registro?',
    ),
));
```

### Opciones adicionales

```php
$table->setState(true);          // Mostrar columna Estado
$table->setCheckbox(true);       // Mostrar checkboxes de selección
$table->disablePaging();         // Sin paginación
$table->disableOrdering();       // Sin ordenamiento
$table->disableResponsive();     // Sin responsive
$table->addParameter('filtro', 'activo');  // Parámetro extra en URLs
$table->addAlias('Estado', array(1 => 'Activo', 0 => 'Inactivo'));  // Alias de valores
$table->setCriteria(array(
    'WHERE' => array(array('name' => 'Estado', 'value' => 1)),
    'ORDER' => 'Nombre ASC',
));
```

## Registro en Modules.php

Si usaste `--no-module-registry`, debes agregar manualmente la entrada en `app/config/Modules.php`:

```php
'producto' => array(
    'enabled'    => true,
    'controller' => 'producto',
    'metadata'   => array(
        'icon'     => '<i class="bi bi-box fs-2"></i>',
        'category' => 'Herramientas',
        'order'    => 50,
    ),
    'menu' => array(
        'name'   => 'herramientas_producto',
        'title'  => 'Productos',
        'action' => 'list',
        'group'  => 'herramientas',
    ),
    'quick_actions' => array(
        array(
            'id'          => 'qa-producto-nuevo',
            'label'       => 'Nuevo Producto',
            'keywords'    => array('crear', 'nuevo', 'producto'),
            'path'        => array('controller' => 'producto', 'action' => 'create'),
            'module'      => 'Herramientas',
            'permissions' => array(array('controller' => 'Producto', 'action' => 'create')),
        ),
    ),
),
```

## Reglas técnicas

1. **El controlador orquesta** — no debe contener SQL ni lógica de datos.
2. **El modelo concentra datos** — toda query va en el modelo.
3. **La vista solo renderiza** — sin lógica de persistencia.
4. **Toda acción nueva** debe registrarse en `loadAccessControl()`.
5. **Cada formulario POST** debe incluir el token CSRF.
6. **Nombres de campos POST** siguen `NombreModelo[NombreAtributo]`.
7. **`Menu::setActive()`** debe llamarse en cada acción para resaltar el menú correcto.

## Checklist del módulo

- [ ] Controller creado con `loadAccessControl()` y todas las acciones.
- [ ] Model con `getOptionsAttributes()` definidos.
- [ ] Migración creada y reversible (`up`/`down`).
- [ ] Seeder de permisos creado y ejecutado.
- [ ] Vistas creadas: list, view, create, edit, _form.
- [ ] Registro en `Modules.php` con menú y quick actions.
- [ ] Token CSRF en todos los formularios.
- [ ] `Menu::setActive()` en cada acción.
- [ ] Lint PHP sin errores en archivos modificados.
