Crear un Mantenedor desde Cero

GuΓ­a paso a paso para crear la pantalla de administraciΓ³n de Trazas sin usar el generador de cΓ³digo, entendiendo cada capa de la arquitectura.

ΒΏQuΓ© es un Mantenedor?

Un mantenedor es una pantalla que permite al usuario administrar los registros de una tabla de base de datos: listar, crear, editar y eliminar. Es el CRUD bΓ‘sico del sistema.

En este proyecto, un mantenedor completo se compone de tres capas de cΓ³digo PHP mΓ‘s una capa de JavaScript en el frontend:

CapaQuΓ© haceArchivos involucrados
BD Base de Datos Tabla SQL Server donde viven los datos Tools.Traza
DAO Acceso a Datos Mapea la tabla a PHP, expone operaciones CRUD Entity, Dao
BLL LΓ³gica de Negocio DTOs, servicios de mapeo y reglas de negocio Dto, Svc, BO
CTRL Controllers Reciben peticiones HTTP y devuelven respuestas Controller web + Controller API
VIEW Vistas PHP Renderizan el HTML usando componentes del framework verView, verGridView, popupView, verGridT
JS JavaScript LΓ³gica del frontend: llamadas AJAX, validaciones, eventos TrazaSvc.js, traza.js
πŸ’‘ Antes de empezar En este proyecto existe un generador de cΓ³digo (bin/gen.sh) que crea automΓ‘ticamente la mayorΓ­a de estos archivos. Esta guΓ­a te enseΓ±a a hacerlo manualmente para que entiendas cada pieza.

Arquitectura en Capas

El sistema sigue una arquitectura en capas estricta. Ninguna capa puede saltarse a otra: la PresentaciΓ³n solo habla con BLL, BLL solo habla con DAO.

PRESENTACIΓ“N
HTML + JS + PHP (MVC) Β· Controller β†’ Vista
↕  DTO  (Data Transfer Object β€” objeto plano sin lΓ³gica)
BLL β€” Business Logic Layer
Svc (mapeo) Β· BO (reglas de negocio)
↕  Entity  (objeto que mapea directamente a la tabla)
DAO β€” Data Access Objects
Operaciones CRUD contra SQL Server

Regla de dependencias

CapaConoceNO conoce
PresentaciΓ³nDTOsEntities, DAOs directamente
BLLDTOs + EntitiesSQL, conexiones directas
DAOEntitiesDTOs, lΓ³gica de negocio

Flujo de una operaciΓ³n

Usuario β†’ Controller β†’ BO β†’ Svc β†’ Dao β†’ SQL Server
                     ←   DTO  ←  DTO ←
⚠️ Regla de oro El Controller nunca llama al Dao directamente. El BO nunca genera HTML. El Dao nunca contiene reglas de negocio.

Archivos a Crear

Para la tabla Tools.Traza necesitas crear los siguientes archivos, en este orden:

Application/ β”œβ”€β”€ Dao/ β”‚ β”œβ”€β”€ Entities/Tools/ β”‚ β”‚ β”œβ”€β”€ Traza.php ← Entity (tabla) β”‚ β”‚ β”œβ”€β”€ TrazaT.php ← Trait de Entity (extensiones) β”‚ β”‚ β”œβ”€β”€ VWTraza.php ← Entity (vista de BD) β”‚ β”‚ └── VWTrazaT.php ← Trait de VWEntity β”‚ └── Services/Tools/ β”‚ β”œβ”€β”€ TrazaDao.php ← DAO Service β”‚ β”œβ”€β”€ TrazaDaoT.php ← Trait de DAO β”‚ β”œβ”€β”€ VWTrazaDao.php ← VW DAO Service β”‚ └── VWTrazaDaoT.php ← Trait de VW DAO β”œβ”€β”€ BLL/ β”‚ β”œβ”€β”€ DataTransferObjects/Tools/ β”‚ β”‚ β”œβ”€β”€ TrazaDto.php ← DTO β”‚ β”‚ β”œβ”€β”€ TrazaDtoT.php ← Trait de DTO β”‚ β”‚ β”œβ”€β”€ VWTrazaDto.php ← VW DTO (para bΓΊsquedas) β”‚ β”‚ └── VWTrazaDtoT.php ← Trait de VW DTO β”‚ β”œβ”€β”€ Services/Tools/ β”‚ β”‚ β”œβ”€β”€ TrazaSvc.php ← BLL Service β”‚ β”‚ β”œβ”€β”€ TrazaSvcT.php ← Trait de Svc β”‚ β”‚ β”œβ”€β”€ VWTrazaSvc.php ← VW Svc (para bΓΊsquedas) β”‚ β”‚ └── VWTrazaSvcT.php ← Trait de VW Svc β”‚ └── BusinessObjects/Mantenedores/Tools/ β”‚ β”œβ”€β”€ TrazaBO.php ← Business Object (lΓ³gica) β”‚ └── TrazaBOT.php ← Trait del BO (flags + validaciones) β”œβ”€β”€ Controllers/ β”‚ β”œβ”€β”€ Mantenedores/Tools/ β”‚ β”‚ β”œβ”€β”€ TrazaController.php ← Controller Web (renderiza la vista) β”‚ β”‚ └── TrazaControllerT.php ← Trait del Controller β”‚ └── Api/Mantenedores/Tools/ β”‚ β”œβ”€β”€ TrazaController.php ← Controller API (endpoints AJAX) β”‚ └── TrazaControllerT.php ← Trait del Controller API └── Views/Mantenedores/Tools/Traza/ β”œβ”€β”€ verView.php ← Vista principal (filtros + contenedor) β”œβ”€β”€ verGridView.php ← Vista de la grilla de resultados β”œβ”€β”€ popupView.php ← Vista del formulario modal (crear/editar) └── verGridT.php ← Template de la grilla (orden, personalizaciΓ³n)
public/ β”œβ”€β”€ services/mantenedores/Tools/ β”‚ β”œβ”€β”€ TrazaSvc.js ← Cliente AJAX del mantenedor β”‚ └── TrazaSvcT.js ← Extensiones JS (puede quedar vacΓ­o) └── scripts/mantenedores/tools/ β”œβ”€β”€ traza.js ← Eventos y lΓ³gica frontend └── trazaT.js ← Extensiones (puede quedar vacΓ­o)
πŸ’‘ El patrΓ³n Trait (sufijo T) Cada archivo principal tiene un hermano Trait con el sufijo T. La clase base contiene la estructura que podrΓ­a regenerarse; el Trait contiene las extensiones manuales que nunca se deben perder. Si no tienes extensiones, el Trait queda vacΓ­o pero siempre debe existir.

DAO Entity β€” Traza.php

La Entity es un objeto PHP plano (POCO) que representa exactamente una tabla de base de datos. Sus propiedades mapean columna por columna a la tabla SQL.

⚠️ Regla importante La Entity no contiene lógica. Solo declara propiedades tipadas con valores por defecto. El constructor estÑ vacío.

UbicaciΓ³n del archivo

Application/Dao/Entities/Tools/Traza.php

CΓ³digo completo

<?php
namespace Application\Dao\Entities\Tools;

use Intouch\Framework\Annotation\Attributes\Entity;
use Intouch\Framework\Annotation\Attributes\EntityField;

// El atributo #[Entity] le indica al framework a quΓ© schema de BD pertenece
#[Entity(Schema: 'Tools')]
class Traza {
    use TrazaT;   // Trait hermano β€” extensiones manuales van ahΓ­

    // PrimaryKey: true β†’ campo autoincremental, siempre parte en 0
    #[EntityField(PrimaryKey: true)]
    public int    $IdTraza = 0;

    // Campos normales β€” tipados, con valor por defecto
    public string $Descripcion    = '';
    public int    $IndVigente     = 0;
    public int    $IndEliminado   = 0;

    function __construct() { }
}

Anotaciones disponibles

AnotaciΓ³nCuΓ‘ndo usarla
#[Entity(Schema: 'X')]Siempre. Define el schema de BD (ej: Tools, Core)
#[EntityField(PrimaryKey: true)]En el campo ID autoincremental (solo uno por Entity)
#[EntityField(DataType: 'datetime')]En campos de fecha/hora (se almacenan como string en PHP)

Tipos PHP segΓΊn columna SQL

Tipo SQL ServerTipo PHPValor por defecto
int, tinyint, smallintint0
varchar, nvarchar, charstring''
datetime, datestring + #[EntityField(DataType:'datetime')]''
decimal, floatfloat0.0
Columna nullable?tiponull

DAO Trait de Entity β€” TrazaT.php

El Trait hermano de la Entity existe para que puedas agregar mΓ©todos o propiedades sin tocar la clase base (que podrΓ­a regenerarse). Si no necesitas nada extra, lo dejas vacΓ­o.

UbicaciΓ³n del archivo

Application/Dao/Entities/Tools/TrazaT.php

CΓ³digo β€” vacΓ­o (mΓ­nimo requerido)

<?php
namespace Application\Dao\Entities\Tools;

trait TrazaT {
    // Agrega aquΓ­ mΓ©todos o propiedades extra si los necesitas
}
πŸ’‘ ΒΏCuΓ‘ndo agregar algo aquΓ­? Si el framework necesita que la Entity tenga algΓΊn comportamiento personalizado. En la prΓ‘ctica, para un mantenedor simple como Traza, siempre queda vacΓ­o.

DAO VW Entity β€” VWTraza.php

La VWEntity representa una vista de base de datos (prefijo VW). Se usa para las bΓΊsquedas y listados, ya que una vista puede tener JOINs y campos calculados que la tabla base no tiene.

Para Traza, que no tiene llaves forΓ‘neas, la vista tiene las mismas columnas que la tabla. Pero el patrΓ³n siempre se respeta.

πŸ’‘ ConvenciΓ³n de vistas Si la tabla Tools.Traza tuviera un campo IdProfesion (FK hacia tabla Profesion), la vista VWTraza deberΓ­a hacer el JOIN y agregar un campo Profesion (sin el prefijo Id) con la descripciΓ³n correspondiente.

UbicaciΓ³n del archivo

Application/Dao/Entities/Tools/VWTraza.php

CΓ³digo completo

<?php
namespace Application\Dao\Entities\Tools;

use Intouch\Framework\Annotation\Attributes\Entity;
use Intouch\Framework\Annotation\Attributes\EntityField;

#[Entity(Schema: 'Tools')]
class VWTraza {
    use VWTrazaT;

    // Nota: la VW NO tiene #[EntityField(PrimaryKey: true)]
    // porque es una vista de solo lectura
    public int    $IdTraza = 0;
    public string $Descripcion    = '';
    public int    $IndVigente     = 0;
    public int    $IndEliminado   = 0;

    function __construct() { }
}

Diferencias entre Entity y VWEntity

CaracterΓ­sticaEntity (tabla)VWEntity (vista)
Tiene PrimaryKey: trueβœ… Sí❌ No
Se usa para escritura (Insert/Update/Delete)βœ… Sí❌ No
Se usa para bΓΊsqueda/listadoPuede, pero no es lo idealβœ… SΓ­
Puede tener campos calculados o de JOIN❌ Noβœ… SΓ­

Trait de VW Entity β€” VWTrazaT.php

Application/Dao/Entities/Tools/VWTrazaT.php
<?php
namespace Application\Dao\Entities\Tools;

trait VWTrazaT {
    // VacΓ­o para mantenedor simple
}

DAO DAO Service β€” TrazaDao.php

El DAO es la clase que expone las operaciones de persistencia para una Entity. Hereda de GenericDao, que ya implementa todas las operaciones estΓ‘ndar. En la mayorΓ­a de los casos, el cuerpo queda prΓ‘cticamente vacΓ­o: solo el constructor.

UbicaciΓ³n del archivo

Application/Dao/Services/Tools/TrazaDao.php

CΓ³digo completo

<?php
namespace Application\Dao\Services\Tools;

use Application\Dao\Entities\Tools\Traza;
use Intouch\Framework\Dao\GenericDao;

class TrazaDao extends GenericDao {
    use TrazaDaoT;

    function __construct($domain) {
        // Le indica a GenericDao quΓ© Entity manejar y a quΓ© conexiΓ³n conectarse
        parent::__construct(
            Traza::class,
            $domain
        );
    }
}

ΒΏQuΓ© es $domain?

Es la clave de conexiΓ³n definida en Application/Configuration/connection.config.json. Siempre se pasa desde la capa BLL usando la constante ConnectionEnum::DEFAULT. Nunca pongas el string directamente.

Operaciones que hereda de GenericDao

MΓ©todoQuΓ© hace
Find($id)Obtiene un registro por su PK
FindBy($bindings)Obtiene el primer registro que cumple los filtros
GetAll()Obtiene todos los registros de la tabla
GetBy($bindings)Obtiene todos los registros que cumplen los filtros
Insert($entity)Inserta un registro, retorna la Entity con el nuevo ID
Update($entity)Actualiza un registro por su PK
Delete($id)Elimina fΓ­sicamente un registro
LogicDelete($id)Marca IndEliminado = 1 (soft delete)
CountBy($bindings)Cuenta registros que cumplen los filtros

Trait del DAO β€” TrazaDaoT.php

Application/Dao/Services/Tools/TrazaDaoT.php
<?php
namespace Application\Dao\Services\Tools;

trait TrazaDaoT {
    // Agrega aquΓ­ mΓ©todos DAO adicionales si los necesitas
    // Por ejemplo, consultas SQL personalizadas con stored procedures
}

DAO VW DAO Service β€” VWTrazaDao.php

Igual que el DAO de la tabla, pero apunta a la VWEntity. Se usa exclusivamente para operaciones de lectura (listados y bΓΊsquedas).

UbicaciΓ³n del archivo

Application/Dao/Services/Tools/VWTrazaDao.php

CΓ³digo completo

<?php
namespace Application\Dao\Services\Tools;

use Application\Dao\Entities\Tools\VWTraza;
use Intouch\Framework\Dao\GenericDao;

class VWTrazaDao extends GenericDao {
    use VWTrazaDaoT;

    function __construct($domain) {
        parent::__construct(
            VWTraza::class,   // ← apunta a la VWEntity
            $domain
        );
    }
}

Trait del VW DAO β€” VWTrazaDaoT.php

Application/Dao/Services/Tools/VWTrazaDaoT.php
<?php
namespace Application\Dao\Services\Tools;

trait VWTrazaDaoT {
    // VacΓ­o para mantenedor simple
}
πŸ’‘ Resumen de la capa DAO En total son 8 archivos PHP en la capa DAO, pero la mayorΓ­a son casi vacΓ­os. El framework hace todo el trabajo pesado a travΓ©s de GenericDao. Tu ΓΊnica responsabilidad es indicarle quΓ© Entity manejar.

RelaciΓ³n entre los archivos DAO

ArchivoExtiende / UsaPara quΓ©
Traza.php β€” Representa la tabla (escritura)
TrazaDao.php GenericDao + Traza CRUD sobre la tabla
VWTraza.php β€” Representa la vista (lectura)
VWTrazaDao.php GenericDao + VWTraza BΓΊsquedas y listados

BLL DTO β€” TrazaDto.php

El DTO (Data Transfer Object) es el objeto que viaja entre la capa de PresentaciΓ³n y la capa BLL. Es un objeto plano sin lΓ³gica, con las mismas propiedades que la Entity pero sin las anotaciones del framework DAO.

Usa el constructor con named parameters de PHP 8, lo que permite instanciarlo pasando solo los campos que necesitas.

UbicaciΓ³n del archivo

Application/BLL/DataTransferObjects/Tools/TrazaDto.php

CΓ³digo completo

<?php
namespace Application\BLL\DataTransferObjects\Tools;

class TrazaDto {
    use TrazaDtoT;

    public function __construct(
        // Mismos campos que la Entity, mismos tipos, mismos valores por defecto
        public int    $IdTraza = 0,
        public string $Descripcion    = '',
        public int    $IndVigente     = 0,
        public int    $IndEliminado   = 0,
    ) { }
}

Named parameters β€” ΒΏcΓ³mo se usa?

// Crear un DTO pasando solo los campos necesarios (PHP 8+)
$traza = new TrazaDto(
    IdTraza: 0,
    Descripcion:    'CardiologΓ­a',
    IndVigente:     1,
    IndEliminado:   0
);

// TambiΓ©n vΓ‘lido: solo ID (el resto toma su valor por defecto)
$traza = new TrazaDto(IdTraza: 42);

Trait del DTO β€” TrazaDtoT.php

Application/BLL/DataTransferObjects/Tools/TrazaDtoT.php
<?php
namespace Application\BLL\DataTransferObjects\Tools;

trait TrazaDtoT {
    // El Trait del DTO es el lugar correcto para agregar:
    // - Propiedades calculadas (ej: nombre completo)
    // - Colecciones de objetos relacionados (ej: public array $Profesionales = [])
    // - __get / __set para acceso dinΓ‘mico a propiedades no declaradas
}
πŸ’‘ PersonalizaciΓ³n en DtoT, nunca en Dto El archivo TrazaDto.php puede regenerarse con bin/gen.sh. Si agregas cΓ³digo ahΓ­, lo perderΓ‘s. Todo lo personalizado va en TrazaDtoT.php.

BLL VW DTO β€” VWTrazaDto.php

Igual que el DTO, pero para la vista. Se usa para los resultados de bΓΊsqueda que llena la grilla.

UbicaciΓ³n del archivo

Application/BLL/DataTransferObjects/Tools/VWTrazaDto.php

CΓ³digo completo

<?php
namespace Application\BLL\DataTransferObjects\Tools;

class VWTrazaDto {
    use VWTrazaDtoT;

    public function __construct(
        public int    $IdTraza = 0,
        public string $Descripcion    = '',
        public int    $IndVigente     = 0,
        public int    $IndEliminado   = 0,
    ) { }
}

Trait β€” VWTrazaDtoT.php

<?php
namespace Application\BLL\DataTransferObjects\Tools;

trait VWTrazaDtoT {
    // VacΓ­o para mantenedor simple
}

BLL Service β€” TrazaSvc.php

El Service (Svc) conecta el trΓ­o DTO + Entity + Dao. Es quien sabe cΓ³mo mapear un DTO a una Entity y viceversa. Hereda de GenericSvc, que ya implementa todo el mapeo y expone las mismas operaciones que el DAO pero trabajando con DTOs.

UbicaciΓ³n del archivo

Application/BLL/Services/Tools/TrazaSvc.php

CΓ³digo completo

<?php
namespace Application\BLL\Services\Tools;

use Application\Dao\Services\Tools\TrazaDao;
use Application\BLL\DataTransferObjects\Tools\TrazaDto;
use Application\Dao\Entities\Tools\Traza;
use Intouch\Framework\BLL\Service\GenericSvc;

class TrazaSvc extends GenericSvc {
    use TrazaSvcT;

    function __construct($domain) {
        parent::__construct(
            TrazaDto::class,    // ← el DTO que maneja
            Traza::class,       // ← la Entity correspondiente
            new TrazaDao($domain)  // ← el DAO que usa internamente
        );
    }
}

Operaciones que hereda de GenericSvc

Las mismas que GenericDao pero recibe y retorna DTOs en lugar de Entities:

MΓ©todoEntradaSalida
Find($id)int (PK)TrazaDto
GetBy($bindings)array de BindVariablearray de TrazaDto
Insert($dto)TrazaDtoTrazaDto (con nuevo ID)
Update($dto)TrazaDtovoid
Delete($id)int (PK)void
LogicDelete($id)int (PK)void (pone IndEliminado = 1)
CountBy($bindings)array de BindVariableint

Transacciones β€” mΓ©todos estΓ‘ticos de GenericSvc

// Iniciar transacciΓ³n
GenericSvc::BeginMultipleOperations(ConnectionEnum::DEFAULT);

// ... operaciones Insert/Update/Delete ...

// Confirmar
GenericSvc::SaveMultipleOperations();

// O revertir (en el catch)
GenericSvc::UndoMultipleOperations();

Trait del Svc β€” TrazaSvcT.php

Application/BLL/Services/Tools/TrazaSvcT.php
<?php
namespace Application\BLL\Services\Tools;

trait TrazaSvcT {
    // AquΓ­ puedes definir $innerMappings = [] para mapeos especiales DTO ↔ Entity
    // VacΓ­o para mantenedor simple
}

BLL VW Service β€” VWTrazaSvc.php

El Svc de la vista, que se usa exclusivamente para bΓΊsquedas con filtros en el BO.

UbicaciΓ³n del archivo

Application/BLL/Services/Tools/VWTrazaSvc.php

CΓ³digo completo

<?php
namespace Application\BLL\Services\Tools;

use Application\Dao\Services\Tools\VWTrazaDao;
use Application\BLL\DataTransferObjects\Tools\VWTrazaDto;
use Application\Dao\Entities\Tools\VWTraza;
use Intouch\Framework\BLL\Service\GenericSvc;

class VWTrazaSvc extends GenericSvc {
    use VWTrazaSvcT;

    function __construct($domain) {
        parent::__construct(
            VWTrazaDto::class,
            VWTraza::class,
            new VWTrazaDao($domain)
        );
    }
}

Trait β€” VWTrazaSvcT.php

<?php
namespace Application\BLL\Services\Tools;

trait VWTrazaSvcT {
    // VacΓ­o para mantenedor simple
}
βœ… ΒΏQuΓ© tenemos hasta ahora? Con las 4 partes anteriores ya tienes toda la infraestructura de acceso a datos lista. El siguiente paso es el Business Object (BO), donde vive la lΓ³gica de negocio real.

BLL Business Object β€” TrazaBO.php

El Business Object (BO) es el corazΓ³n de la lΓ³gica de negocio. No hereda de ninguna clase base del framework. Contiene un mΓ©todo por cada caso de uso del mantenedor: guardar, buscar, eliminar, validar.

El Controller instancia el BO y le delega todo. El BO nunca genera HTML ni conoce controles de UI.

UbicaciΓ³n del archivo

Application/BLL/BusinessObjects/Mantenedores/Tools/TrazaBO.php

CΓ³digo completo

<?php
namespace Application\BLL\BusinessObjects\Mantenedores\Tools;

use Application\BLL\Services\Tools\TrazaSvc;
use Application\BLL\Services\Tools\VWTrazaSvc;
use Application\BLL\DataTransferObjects\Tools\TrazaDto;
use Application\Configuration\ConnectionEnum;
use Intouch\Framework\BLL\Service\GenericSvc;
use Intouch\Framework\Dao\BindVariable;
use Intouch\Framework\Exceptions\BusinessException;
use Intouch\Framework\Exceptions\ExceptionCodesEnum;
use ReflectionClass;

class TrazaBO {

    use TrazaBOT;   // ← flags y validaciones en el Trait

    // ─────────────────────────────────────────────────────────────
    // GUARDAR (actualizar registro existente)
    // ─────────────────────────────────────────────────────────────
    public function GuardarTraza($registro) {

        $trazaSvc = new TrazaSvc(ConnectionEnum::DEFAULT);

        try {
            GenericSvc::BeginMultipleOperations(ConnectionEnum::DEFAULT);

            $traza = new TrazaDto(
                IdTraza: $registro->IdTraza,
                Descripcion:    $registro->Descripcion,
                IndVigente:     $registro->IndVigente,
                IndEliminado:   0   // nunca se actualiza por esta vΓ­a
            );

            $trazaSvc->Update($traza);

            GenericSvc::SaveMultipleOperations();
            return true;
        }
        catch(\Exception $e) {
            GenericSvc::UndoMultipleOperations();
            throw $e;
        }
    }

    // ─────────────────────────────────────────────────────────────
    // GUARDAR NUEVO (insertar registro)
    // ─────────────────────────────────────────────────────────────
    public function GuardarNuevaTraza($registro) {

        $trazaSvc = new TrazaSvc(ConnectionEnum::DEFAULT);

        try {
            GenericSvc::BeginMultipleOperations(ConnectionEnum::DEFAULT);

            // Garantizar que se crea un registro nuevo (PK = 0)
            $registro->IdTraza = 0;

            $traza = new TrazaDto(
                IdTraza: $registro->IdTraza,
                Descripcion:    $registro->Descripcion,
                IndVigente:     1,   // los nuevos registros parten vigentes
                IndEliminado:   0
            );

            $nuevaTraza = $trazaSvc->Insert($traza);

            // Verificar que el insert fue exitoso
            if (!isset($nuevaTraza) || $nuevaTraza->IdTraza <= 0) {
                GenericSvc::UndoMultipleOperations();
                throw new \Exception("Ha ocurrido un problema al guardar el registro", 1);
            }

            GenericSvc::SaveMultipleOperations();
            return true;
        }
        catch(\Exception $e) {
            GenericSvc::UndoMultipleOperations();
            throw $e;
        }
    }

    // ─────────────────────────────────────────────────────────────
    // BUSCAR (listado con filtros)
    // ─────────────────────────────────────────────────────────────
    public function BuscarTraza($filtros) {

        // Usamos VWTrazaSvc para la bΓΊsqueda (vista de BD)
        $trazaSvc = new VWTrazaSvc(ConnectionEnum::DEFAULT);

        $bindings = [];

        // Flag del Trait: ocultar eliminados lΓ³gicamente
        if ($this->OcultarEliminadosEnBusqueda) {
            $bindings[] = new BindVariable("IndEliminado", "=", 0);
        }

        // Filtro por ID exacto
        if (isset($filtros->IdTraza) && $filtros->IdTraza != "-1") {
            $bindings[] = new BindVariable("IdTraza", "=", $filtros->IdTraza);
        }

        // Filtro por descripciΓ³n (bΓΊsqueda parcial)
        if (isset($filtros->Descripcion) && $filtros->Descripcion != "") {
            $bindings[] = new BindVariable("Descripcion", "LIKE", '%' . $filtros->Descripcion . '%');
        }

        return $trazaSvc->GetBy(
            bindings: $bindings,
            avoidNull: true   // evita nulls en el resultado
        );
    }

    // ─────────────────────────────────────────────────────────────
    // VALIDAR ELIMINACIΓ“N (retorna mensaje si no se puede eliminar)
    // ─────────────────────────────────────────────────────────────
    public function SePuedeEliminarTraza($IdTraza) {

        $mensajes = $this->ValidarEliminarTraza($IdTraza);

        if (count($mensajes) > 0) {
            return implode('<br>', $mensajes);
        }

        return "";   // string vacΓ­o = se puede eliminar sin advertencias
    }

    // ─────────────────────────────────────────────────────────────
    // ELIMINAR
    // ─────────────────────────────────────────────────────────────
    public function EliminarTraza($IdTraza) {

        GenericSvc::BeginMultipleOperations(ConnectionEnum::DEFAULT);

        try {
            // Eliminar dependencias en cascada (definidas en el Trait)
            $this->EliminarElementosDependientesTraza($IdTraza);

            $trazaSvc = new TrazaSvc(ConnectionEnum::DEFAULT);

            // Flag del Trait: eliminaciΓ³n lΓ³gica (IndEliminado=1) o fΓ­sica
            if ($this->EliminacionLogica) {
                $trazaSvc->LogicDelete($IdTraza);   // soft delete
            }
            else {
                $trazaSvc->Delete($IdTraza);          // hard delete
            }

            GenericSvc::SaveMultipleOperations();
        }
        catch (\Exception $e) {
            GenericSvc::UndoMultipleOperations();
            throw new BusinessException(
                ExceptionCodesEnum::ERR_DATA_DELETE,
                'al intentar eliminar el registro de Traza'
            );
        }

        return '';
    }

    // ─────────────────────────────────────────────────────────────
    // OBTENER DATA (para cargar el popup nuevo o ediciΓ³n)
    // ─────────────────────────────────────────────────────────────
    public function ObtenerDataTraza(
        $IdTraza  = 0,
        $NombreIdPadre   = '',
        $ValorIdPadre    = -1
    ) {
        $data = [];

        if ($IdTraza != 0) {
            // EdiciΓ³n: cargar el registro existente
            $trazaSvc   = new TrazaSvc(ConnectionEnum::DEFAULT);
            $data['Elemento'] = $trazaSvc->Find($IdTraza);
        }
        else {
            // Nuevo: crear un DTO vacΓ­o
            $data['Elemento'] = new TrazaDto(IdTraza: 0);
        }

        return (object)$data;
    }
}

BindVariable β€” filtros dinΓ‘micos

El objeto BindVariable es la forma correcta de pasar filtros a GetBy, FindBy y CountBy:

OperadorEjemplo
=new BindVariable("IndEliminado", "=", 0)
LIKEnew BindVariable("Descripcion", "LIKE", '%cardio%')
>, <, >=, <=, <>new BindVariable("IdTraza", ">", 10)
INnew BindVariable("IdTraza", "IN", [1,2,3])

BLL Trait del BO β€” TrazaBOT.php

El Trait del BO contiene los flags de comportamiento y los mΓ©todos de validaciΓ³n y eliminaciΓ³n en cascada. Es el lugar donde personalizas el comportamiento del mantenedor sin tocar la clase base.

UbicaciΓ³n del archivo

Application/BLL/BusinessObjects/Mantenedores/Tools/TrazaBOT.php

CΓ³digo completo

<?php
namespace Application\BLL\BusinessObjects\Mantenedores\Tools;

use Application\Configuration\ConnectionEnum;
use Intouch\Framework\Dao\BindVariable;

trait TrazaBOT {

    // ── FLAGS DE COMPORTAMIENTO ──────────────────────────────────

    // true  β†’ EliminarTraza usarΓ‘ LogicDelete (IndEliminado=1)
    // false β†’ usarΓ‘ Delete (borrado fΓ­sico)
    public $EliminacionLogica           = true;

    // true  β†’ BuscarTraza filtrarΓ‘ WHERE IndEliminado = 0
    public $OcultarEliminadosEnBusqueda = true;

    // ── VALIDACIONES DE ELIMINACIΓ“N ──────────────────────────────
    public function ValidarEliminarTraza($IdTraza) : array {

        $mensajes = [];

        // Ejemplo: verificar si hay Profesionales asociados antes de eliminar
        //
        // $profesionalSvc = new ProfesionalSvc(ConnectionEnum::DEFAULT);
        // $cantidad = $profesionalSvc->CountBy([
        //     new BindVariable('IndEliminado', '=', 0),
        //     new BindVariable('IdTraza', '=', $IdTraza)
        // ]);
        //
        // if ($cantidad > 0) {
        //     $mensajes[] = "Existen $cantidad profesional(es) asociados a esta traza.";
        // }

        return $mensajes;
    }

    // ── ELIMINACIΓ“N EN CASCADA ───────────────────────────────────
    public function EliminarElementosDependientesTraza($IdTraza) {

        // Ejemplo: eliminar los profesionales de esta traza antes de eliminarla
        // (new ProfesionalBO())->EliminarByTraza($IdTraza);

        return;
    }
}

ΒΏPor quΓ© separar el Trait del BO?

TrazaBO.phpTrazaBOT.php
Estructura fija: los 5 mΓ©todos del mantenedor (Guardar, GuardarNuevo, Buscar, Eliminar, ObtenerData) Personalizaciones: flags, validaciones antes de eliminar, eliminaciΓ³n en cascada
PodrΓ­a regenerarse Siempre se edita manualmente, nunca se sobreescribe
⚠️ ¿CuÑndo cambio EliminacionLogica a false? Cuando la tabla no tiene el campo IndEliminado o cuando realmente necesitas borrar el registro de la base de datos. Para la mayoría de los mantenedores del sistema, la eliminación lógica es la opción correcta.

CTRL Controller Web β€” TrazaController.php

El Controller Web tiene una sola responsabilidad: renderizar la vista principal cuando el usuario navega a la URL del mantenedor. Es el punto de entrada de la ruta web (no AJAX).

En un mantenedor simple, este controller tiene un ΓΊnico mΓ©todo: Ver().

UbicaciΓ³n del archivo

Application/Controllers/Mantenedores/Tools/TrazaController.php

CΓ³digo completo

<?php
namespace Application\Controllers\Mantenedores\Tools;

use Application\BLL\BusinessObjects\Mantenedores\Tools\TrazaBO;
use Application\Resources\AssetManagerFactory;
use Intouch\Framework\Annotation\Attributes\ReturnViewResult;
use Intouch\Framework\Annotation\Attributes\Route;
use Intouch\Framework\Controllers\BaseController;

// Seguridad por defecto en toda la clase
#[Route(Authorization: 'ALLOW_ALL', RequireSession: true)]
class TrazaController extends BaseController {
    use TrazaControllerT;

    public function __construct() {
        parent::__construct(assetManagerFactory: new AssetManagerFactory());
    }

    // Solo ADMIN puede acceder a este mantenedor
    #[Route(Authorization: 'ENFORCE', Roles: ['ADMIN'], RequireSession: true)]
    #[ReturnViewResult]
    public function Ver() {

        // Preparar datos iniciales para la vista (DTO vacΓ­o, listas de selects, etc.)
        $trazaBO = new TrazaBO();
        $data = $trazaBO->ObtenerDataTraza();

        // RenderView busca la vista en: Views/Mantenedores/Tools/Traza/verView.php
        return $this->RenderView('ver', $data);
    }
}

Anotaciones de ruta y seguridad

AnotaciΓ³nSignificado
#[Route(Authorization: 'ALLOW_ALL')]Por defecto permite acceso (se refina en cada mΓ©todo)
#[Route(Authorization: 'ENFORCE', Roles: ['ADMIN'])]Solo usuarios con el rol ADMIN pueden ejecutar este mΓ©todo
#[Route(RequireSession: true)]Requiere sesiΓ³n iniciada; redirige al login si no la hay
#[ReturnViewResult]Le indica al framework que este mΓ©todo retorna HTML renderizado

ΒΏCΓ³mo resuelve RenderView la vista?

El mΓ©todo RenderView('ver', $data) busca el archivo:

Application/Views/[namespace del controller]/Traza/verView.php

Como el namespace es Mantenedores/Tools, buscarΓ‘:

Application/Views/Mantenedores/Tools/Traza/verView.php

Trait del Controller Web β€” TrazaControllerT.php

Application/Controllers/Mantenedores/Tools/TrazaControllerT.php
<?php
namespace Application\Controllers\Mantenedores\Tools;

trait TrazaControllerT {
    // MΓ©todos adicionales del controller web
}

CTRL Controller API β€” Api/TrazaController.php

El Controller API expone los endpoints AJAX que el JavaScript del frontend llama. Cada mΓ©todo corresponde a una acciΓ³n del mantenedor: buscar, guardar, eliminar, abrir popup.

Todos los mΓ©todos estΓ‘n decorados con #[ReturnActionResult] o #[ReturnActionViewResult] segΓΊn si retornan datos o HTML.

UbicaciΓ³n del archivo

Application/Controllers/Api/Mantenedores/Tools/TrazaController.php

CΓ³digo completo

<?php
namespace Application\Controllers\Api\Mantenedores\Tools;

use Application\BLL\BusinessObjects\Mantenedores\Tools\TrazaBO;
use Application\Resources\AssetManagerFactory;
use Intouch\Framework\Annotation\Attributes\ReturnActionResult;
use Intouch\Framework\Annotation\Attributes\ReturnActionViewResult;
use Intouch\Framework\Annotation\Attributes\Route;
use Intouch\Framework\Controllers\BaseController;
use Intouch\Framework\Exceptions\BusinessException;
use Intouch\Framework\Exceptions\ExceptionCodesEnum;
use Intouch\Framework\View\Display;

#[Route(Authorization: 'ALLOW_ALL', RequireSession: false)]
class TrazaController extends BaseController {

    use TrazaControllerT;

    public function __construct() {
        parent::__construct(assetManagerFactory: new AssetManagerFactory());
    }

    // ─────────────────────────────────────────────────────────────
    // Verificar si se puede eliminar (retorna advertencias)
    // ─────────────────────────────────────────────────────────────
    #[Route(Methods: ['POST'], Authorization: 'ALLOW_ALL', RequireSession: false)]
    #[ReturnActionResult]
    public function SePuedeEliminarTraza($IdTraza) {
        $trazaBO = new TrazaBO();
        return $trazaBO->SePuedeEliminarTraza($IdTraza);
    }

    // ─────────────────────────────────────────────────────────────
    // Eliminar el registro
    // ─────────────────────────────────────────────────────────────
    #[Route(Methods: ['POST'], Authorization: 'ALLOW_ALL', RequireSession: false)]
    #[ReturnActionResult]
    public function EliminarTraza($IdTraza) {
        $trazaBO = new TrazaBO();
        return $trazaBO->EliminarTraza($IdTraza);
    }

    // ─────────────────────────────────────────────────────────────
    // Abrir el popup (nuevo o ediciΓ³n)
    // ─────────────────────────────────────────────────────────────
    #[Route(Methods: ['POST'], Authorization: 'ALLOW_ALL', RequireSession: false)]
    #[ReturnActionViewResult]
    public function PopUpTraza($IdTraza, $NombreIdPadre, $ValorIdPadre) {
        $trazaBO = new TrazaBO();
        $data = $trazaBO->ObtenerDataTraza(
            $IdTraza, $NombreIdPadre, $ValorIdPadre
        );
        // RenderView β†’ Views/Mantenedores/Tools/Traza/popupView.php
        $view = Display::GetRenderer('Mantenedores/Tools/Traza')
                       ->RenderView('popup', $data);
        return $view;
    }

    // ─────────────────────────────────────────────────────────────
    // Guardar (nuevo o existente, diferenciado por ID)
    // ─────────────────────────────────────────────────────────────
    #[Route(Methods: ['POST'])]
    #[ReturnActionResult]
    public function GuardarTraza($TrazaRegistro) {
        $trazaBO = new TrazaBO();

        try {
            // ID = 0 β†’ es nuevo; ID > 0 β†’ actualizar existente
            if (!isset($TrazaRegistro->IdTraza)
                || $TrazaRegistro->IdTraza == 0) {
                $trazaBO->GuardarNuevaTraza($TrazaRegistro);
            }
            else {
                $trazaBO->GuardarTraza($TrazaRegistro);
            }
        }
        catch (\Exception $ex) {
            throw new BusinessException(
                ExceptionCodesEnum::ERR_DATA_INSERT,
                ' al guardar el registro'
            );
        }

        return 0;
    }

    // ─────────────────────────────────────────────────────────────
    // Buscar (retorna HTML de la grilla)
    // ─────────────────────────────────────────────────────────────
    #[Route(Methods: ['POST'], Authorization: 'ALLOW_ALL', RequireSession: false)]
    #[ReturnActionViewResult]
    public function BuscarTraza($RegistroTrazaBuscar) {
        $trazaBO = new TrazaBO();
        $trazas = $trazaBO->BuscarTraza($RegistroTrazaBuscar);

        // RenderView β†’ Views/Mantenedores/Tools/Traza/verGridView.php
        $view = Display::GetRenderer('Mantenedores/Tools/Traza')
                       ->RenderView('verGrid', $trazas);
        return $view;
    }
}

Diferencia entre las anotaciones de retorno

AnotaciΓ³nQuΓ© retornaCuΓ‘ndo usarla
#[ReturnActionResult] JSON con datos primitivos (string, int, bool) Guardar, Eliminar, verificaciones
#[ReturnActionViewResult] JSON con HTML renderizado Buscar (grilla), PopUp (formulario modal)
#[ReturnViewResult] HTML completo (pΓ‘gina entera) Solo en el Controller Web (no API)

Trait del Controller API β€” TrazaControllerT.php

Application/Controllers/Api/Mantenedores/Tools/TrazaControllerT.php
<?php
namespace Application\Controllers\Api\Mantenedores\Tools;

trait TrazaControllerT {
    // Endpoints adicionales personalizados van aquΓ­
}

VIEW Vista Principal β€” verView.php

Esta es la pantalla principal del mantenedor. Contiene el formulario de filtros de bΓΊsqueda, los botones de buscar/limpiar/nuevo, y un contenedor vacΓ­o donde se inyectarΓ‘ la grilla de resultados vΓ­a AJAX.

No contiene HTML crudo: usa los componentes del framework (Display, Panel, Container, FormRowGroup, etc.).

UbicaciΓ³n del archivo

Application/Views/Mantenedores/Tools/Traza/verView.php

Directivas de layout y bundle JS

@@Layout(authenticated)           ← usa el layout de app autenticada
@@IncludeScriptBundle(mantenedorTrazaJS)  ← incluye el JS del mantenedor

CΓ³digo completo

<?php
use Application\Views\Mantenedores\Tools\Traza\verGridT;
use Intouch\Framework\View\Display;
use Intouch\Framework\View\DisplayDefinitions\Button;
use Intouch\Framework\View\DisplayDefinitions\FormButton;
use Intouch\Framework\View\DisplayDefinitions\FormRowFieldText;
use Intouch\Framework\View\DisplayDefinitions\FormRowGroup;
use Intouch\Framework\View\DisplayEvents\ButtonOnClickEvent;
use Intouch\Framework\View\DisplayEvents\FormButtonOnClickEvent;
use Intouch\Framework\Widget\Container;
use Intouch\Framework\Widget\Definitions\ActionButton\ButtonStyleEnum;
use Intouch\Framework\Widget\Panel;
use Intouch\Framework\Widget\Text;
use Intouch\Framework\Widget\TitleDescription;
?>

@@Layout(authenticated)
@@IncludeScriptBundle(mantenedorTrazaJS)

<?php
// ── ENCABEZADO DE PÁGINA ─────────────────────────────────────────
$pageHeader = verGridT::GetHeader();   // tΓ­tulo e Γ­cono definidos en verGridT
$pageHeader->Draw();

$display = new Display();

// ── BOTONES DEL FORMULARIO DE BÚSQUEDA ──────────────────────────
$display->AddButton(
    new FormButton(
        Key:         'btnBuscarTraza',
        FormKey:     'frmencabezado',
        Child:       new Text('Buscar Trazas'),
        ButtonStyle: ButtonStyleEnum::BUTTON_INFO,
        Classes:     ['pull-right'],
        Events:      [new FormButtonOnClickEvent()]
    )
);

$display->AddButton(
    new FormButton(
        Key:         'btnLimpiarTraza',
        FormKey:     'frmencabezado',
        Child:       new Text('Limpiar Filtros'),
        ButtonStyle: ButtonStyleEnum::BUTTON_DEFAULT,
        Styles:      [['margin-right', '8px']],
        Classes:     ['pull-right'],
        Events:      [new FormButtonOnClickEvent()]
    )
);

// ── CAMPOS DE FILTRO ─────────────────────────────────────────────
$rowGroups = [];
$rowGroups[] = new FormRowGroup(
    Key:   'grp-informacion-traza',
    Title: 'Opciones de BΓΊsqueda',
    Rows: [
        [
            // Campo de texto para filtrar por descripciΓ³n
            'Descripcion' => new FormRowFieldText(
                PropertyName: 'Descripcion',
                Label:        'Nombre',
                Colspan:      4
            ),
        ]
    ]
);

$display->AddFormFromObject(
    formKey:      'frmencabezado',
    object:       $data,       // DTO vacΓ­o pasado desde el Controller
    fillData:     false,       // no pre-llenar con datos
    keyFieldName: 'IdTraza',
    rowGroups:    $rowGroups
);

// ── BOTΓ“N NUEVO ──────────────────────────────────────────────────
$display->AddButton(
    new Button(
        Key:         'btnNuevaTraza',
        Child:       new Text('Nuevo'),
        Classes:     ['pull-right'],
        ButtonStyle: ButtonStyleEnum::BUTTON_PRIMARY,
        Events:      [new ButtonOnClickEvent()]
    )
);

// ── LAYOUT GENERAL ───────────────────────────────────────────────
(
    new Container(
        Classes: ['view-content'],
        Children: [
            // Panel superior: filtros de bΓΊsqueda
            new Panel(
                Body: new Container(
                    Children: [
                        $display->Widgets()['frmencabezado'],
                        $display->Widgets()['btnBuscarTraza'],
                        $display->Widgets()['btnLimpiarTraza'],
                    ]
                ),
            ),
            // Panel inferior: contenedor de la grilla (se llena por AJAX)
            new Panel(
                Header: new Container(
                    Children: [
                        $display->Widgets()['btnNuevaTraza'],
                        new TitleDescription(
                            Title:       'Resultado',
                            Description: 'Listado de: Traza'
                        )
                    ]
                ),
                Body: new Container(
                    Key:      'formulario-traza',  ← ID usado por el JS para inyectar la grilla
                    Children: []
                ),
            )
        ]
    )
)->Draw();

$display->DrawScripts();

Componentes clave

ComponenteQuΓ© genera
FormButtonBotΓ³n que envΓ­a un formulario; su evento OnClick llama a btnBuscarTraza_OnClick() en el JS
ButtonBotΓ³n simple; su evento llama a btnNuevaTraza_OnClick()
FormRowFieldTextCampo de texto en el formulario
Container(Key: 'formulario-traza')El <div> donde el JS inyectarΓ‘ la grilla con $('#formulario-traza').html(result)
πŸ’‘ El Key de los componentes importa El Key de cada widget (botΓ³n, formulario, etc.) se convierte en el id HTML del elemento. El JavaScript usa estos IDs para disparar eventos y actualizar el DOM.

VIEW Vista Grilla β€” verGridView.php

Esta vista genera la tabla de resultados. El Controller API la renderiza y la devuelve como HTML al JavaScript, que la inyecta en el contenedor #formulario-traza.

Define las columnas de la tabla, los botones de acciΓ³n por fila (Editar, Eliminar), y delega el orden y visibilidad a verGridT.

UbicaciΓ³n del archivo

Application/Views/Mantenedores/Tools/Traza/verGridView.php

CΓ³digo completo

<?php
use Application\Views\Mantenedores\Tools\Traza\verGridT;
use Intouch\Framework\BLL\Filters\DataTableSettingsFilterDto;
use Intouch\Framework\View\Display;
use Intouch\Framework\View\DisplayDefinitions\TableButton;
use Intouch\Framework\View\DisplayDefinitions\TogglePlacementEnum;
use Intouch\Framework\View\DisplayDefinitions\TableCell;
use Intouch\Framework\View\DisplayEvents\TableButtonOnClickEvent;
use Intouch\Framework\Widget\Container;
use Intouch\Framework\Widget\Definitions\ActionButton\ButtonStyleEnum;
use Intouch\Framework\Widget\FaIcon;
use Intouch\Framework\Widget\Label;
use Intouch\Framework\Widget\Definitions\Label\LabelSizeEnum;
use Intouch\Framework\Widget\Definitions\Label\LabelStyleEnum;

$display = new Display();

// ── DEFINICIΓ“N DE COLUMNAS ───────────────────────────────────────
$cells = [

    // Columna Descripcion β€” texto plano
    'Descripcion' => new TableCell(
        PropertyName: "Descripcion",
        Label:        "Nombre",
    ),

    // Columna IndVigente β€” renderizada como etiqueta SI/NO
    'IndVigente' => new TableCell(
        PropertyName: "IndVigente",
        Label:        "ΒΏVigente?",
        BodyClasses:  ['text-center'],
        FormatFunction: function($element, $cell) {
            return new Label(
                LabelStyle: ($element->IndVigente == 1)
                    ? LabelStyleEnum::LABEL_SUCCESS
                    : LabelStyleEnum::LABEL_LIGHT,
                LabelSize: LabelSizeEnum::LABEL_LARGE,
                Content:   ($element->IndVigente == 1) ? 'SI' : 'NO'
            );
        }
    ),
];

// Aplicar orden y visibilidad de columnas definido en verGridT
$cells = verGridT::GetCellOrderAndVisibility($cells);
foreach ($cells as $cell) {
    verGridT::CustomCell($cell);
}

// ── BOTONES DE ACCIΓ“N POR FILA ───────────────────────────────────
$buttons = verGridT::GetCustomButtons();

// BotΓ³n Editar β†’ llama a btnEditarTraza_OnClick() en el JS
$buttons[] = new TableButton(
    Child:           new FaIcon('fa-edit'),
    Key:             'btnEditarTraza',
    OnClickClass:    'btn-editar-traza',
    ButtonStyle:     ButtonStyleEnum::BUTTON_WARNING,
    TogglePopUp:     true,
    ToggleText:      'Editar Traza',
    TogglePlacement: TogglePlacementEnum::TOP,
    Events:          [new TableButtonOnClickEvent()],
);

// BotΓ³n Eliminar β†’ llama a btnEliminarTraza_OnClick() en el JS
$buttons[] = new TableButton(
    Key:             'btnEliminarTraza',
    Child:           new FaIcon('fa-trash'),
    OnClickClass:    'btn-eliminar-traza',
    ButtonStyle:     ButtonStyleEnum::BUTTON_DANGER,
    TogglePopUp:     true,
    ToggleText:      'Eliminar Traza',
    TogglePlacement: TogglePlacementEnum::TOP,
    Events:          [new TableButtonOnClickEvent()],
);

// ── TABLA DE DATOS ───────────────────────────────────────────────
$display->AddTableFromCollection(
    tableKey:         'tbTraza',
    RowIdFieldName:   'IdTraza',     ← campo que actΓΊa como PK de la fila
    RowAttributeNames: [                    ← atributos data-xxx que se agregan a cada fila
        'IdTraza', 'Descripcion', 'IndVigente', 'IndEliminado'
    ],
    Data:     $data,                        ← array de VWTrazaDto
    Buttons:  $buttons,
    CellDefinitions: $cells,
    TablaSimple: false,
    CustomDataTable: new DataTableSettingsFilterDto(
        HideAllButtons: true
    )
);

(new Container(
    Children: [$display->Widgets()['tbTraza']]
))->Draw();

$display->DrawScripts(addLoadEvent: false);

ΒΏCΓ³mo funciona RowAttributeNames?

El framework agrega atributos data-xxx a cada fila <tr> de la tabla. Cuando el usuario hace click en "Editar" o "Eliminar", el JS lee esos atributos para saber quΓ© registro manipular:

// En el JS, el botΓ³n recibe rowInfo con los datos de la fila
function btnEditarTraza_OnClick(rowInfo) {
    PopUpTraza(rowInfo.RowData.pk);   // pk = IdTraza
}

FormatFunction β€” personalizar celdas

Cuando necesitas mostrar algo diferente al valor crudo del campo, usas FormatFunction:

CasoEjemplo
Etiqueta SI/NORetornar new Label(...)
MonedaRetornar _mon($element->Precio)
Fecha formateadaRetornar (new DateTime($element->Fecha))->format('d-m-Y')
TΓ­tulo + descripciΓ³nRetornar new TitleDescription(...)

VIEW Template de Grilla β€” verGridT.php

El verGridT es una clase estΓ‘tica que actΓΊa como "template de configuraciΓ³n" de la grilla. Define el orden de columnas, el orden de campos del formulario, y permite personalizar celdas y agregar botones extra. Es el archivo mΓ‘s frecuentemente modificado cuando se necesita ajustar el mantenedor.

UbicaciΓ³n del archivo

Application/Views/Mantenedores/Tools/Traza/verGridT.php

CΓ³digo completo

<?php
namespace Application\Views\Mantenedores\Tools\Traza;

use Intouch\Framework\View\DisplayDefinitions\FormRowGroup;
use Intouch\Framework\View\DisplayDefinitions\TableButton;
use Intouch\Framework\View\DisplayEvents\TableButtonOnClickEvent;
use Intouch\Framework\View\DisplayDefinitions\TogglePlacementEnum;
use Intouch\Framework\Widget\Definitions\ActionButton\ButtonStyleEnum;
use Intouch\Framework\Widget\FaIcon;
use Intouch\Framework\Widget\PageHeader;

class verGridT {

    // ── ORDEN DE COLUMNAS DE LA GRILLA ──────────────────────────────
    // Solo las columnas que retornes aquΓ­ aparecerΓ‘n en la tabla.
    // Cambia el orden agregando/quitando elementos del $newArray.
    public static function GetCellOrderAndVisibility($cells, $data = null) : array {
        $newArray = [];
        $newArray[] = $cells['Descripcion'];
        $newArray[] = $cells['IndVigente'];
        return $newArray;
    }

    // ── ORDEN DE CAMPOS DEL FORMULARIO POPUP ────────────────────────
    // Define quΓ© campos aparecen y en quΓ© orden en el modal.
    public static function GetFormOrderAndVisibility($frmGroup, $data) : array {
        $result = [];
        $newfrmGroup = new FormRowGroup(Key: 'frmgrp-traza', Rows: []);
        $row = [];
        $row[] = $frmGroup->Rows[0]['IdTraza'];
        $row[] = $frmGroup->Rows[0]['Descripcion'];
        $row[] = $frmGroup->Rows[0]['IndVigente'];
        $row[] = $frmGroup->Rows[0]['IndEliminado'];
        $newfrmGroup->Rows[] = $row;
        $result[] = $newfrmGroup;
        return $result;
    }

    // ── ORDEN DE CAMPOS DE BÚSQUEDA ─────────────────────────────────
    public static function GetSearchFormOrderAndVisibility($rowGroup) : array {
        $result = [];
        $newrowGroup = new FormRowGroup(Key: 'rowgrp-traza', Rows: []);
        $row = [];
        $row[] = $rowGroup->Rows[0]['Descripcion'];
        $newrowGroup->Rows[] = $row;
        $result[] = $newrowGroup;
        return $result;
    }

    // ── PERSONALIZAR CELDAS INDIVIDUALMENTE ─────────────────────────
    // Modifica propiedades de una celda especΓ­fica.
    // switch por PropertyName para afectar solo esa columna.
    public static function CustomCell($cell) {

        // Ejemplo automΓ‘tico: columnas "IndXxx" muestran label "ΒΏXxx?"
        if (substr($cell->PropertyName, 0, 3) == 'Ind') {
            $cell->Label = 'ΒΏ' . substr($cell->PropertyName, 3) . '?';
        }

        // PersonalizaciΓ³n por columna especΓ­fica
        switch ($cell->PropertyName) {

            // Ejemplo:
            // case "Descripcion":
            //     $cell->BodyClasses = ['text-bold'];
            //     return;

            default: return;
        }
    }

    // ── BOTONES EXTRA POR FILA ──────────────────────────────────────
    // Agrega botones adicionales (ademΓ‘s de Editar y Eliminar).
    // Ej: un botΓ³n "Ver Profesionales" que abre un sub-mantenedor.
    public static function GetCustomButtons($buttons = [], $filtros = null, $isPopup = false) {

        // Ejemplo comentado:
        // $buttons[] = new TableButton(
        //     Child: new FaIcon('fa-users'),
        //     Key: 'btnVerProfesionales',
        //     OnClickClass: 'btn-ver-profesionales',
        //     ButtonStyle: ButtonStyleEnum::BUTTON_PRIMARY,
        //     TogglePopUp: true,
        //     ToggleText: 'Ver Profesionales',
        //     TogglePlacement: TogglePlacementEnum::TOP,
        //     Events: [new TableButtonOnClickEvent()],
        // );

        return $buttons;
    }

    // ── ENCABEZADO DE PÁGINA ────────────────────────────────────────
    // TΓ­tulo, descripciΓ³n e Γ­cono FontAwesome de la pantalla.
    public static function GetHeader() {
        return new PageHeader(
            Title:       'Trazas',
            Description: 'Seleccione un elemento para editar, o agregue elementos nuevos',
            IconName:    'fa-stethoscope'
        );
    }
}

GuΓ­a rΓ‘pida de modificaciones comunes en verGridT

QuΓ© quiero hacerDΓ³nde y cΓ³mo
Ocultar una columna de la grilla GetCellOrderAndVisibility: no la agregues al $newArray
Cambiar el orden de las columnas GetCellOrderAndVisibility: cambia el orden de las lΓ­neas $newArray[] = ...
Cambiar el label de una columna CustomCell: case "MiCampo": $cell->Label = 'Mi Etiqueta'; return;
Ocultar un campo del formulario modal GetFormOrderAndVisibility: no lo incluyas en el $row
Agregar un botΓ³n de acciΓ³n por fila GetCustomButtons: agrega un new TableButton(...)
Cambiar el tΓ­tulo/Γ­cono de la pantalla GetHeader: modifica Title, Description e IconName
πŸ’‘ Íconos disponibles El proyecto usa FontAwesome 5. Puedes usar cualquier Γ­cono del set free solid con el prefijo fa-. Ejemplos: fa-stethoscope, fa-user-md, fa-hospital, fa-cog, fa-list.

JS Cliente AJAX β€” TrazaSvc.js

Este archivo define la clase JavaScript que encapsula todas las llamadas AJAX al Controller API. Sigue el mismo patrΓ³n que el Svc PHP: un mΓ©todo por endpoint. El frontend nunca llama directamente a una URL; siempre usa esta clase.

UbicaciΓ³n del archivo

public/services/mantenedores/Tools/TrazaSvc.js

CΓ³digo completo

class TrazaSvc extends Service {

    constructor() {
        // Define el prefijo de ruta del Controller API
        // Se resuelve como: /api/mantenedores/tools/traza/{metodo}
        super('mantenedores/tools/traza');
    }

    // Verifica si se puede eliminar (retorna advertencias como HTML)
    SePuedeEliminarTraza(idTraza, onSuccessCallback = null, onErrorCallback = null) {
        this.Call(
            'SePuedeEliminarTraza',
            'post',
            { IdTraza: idTraza },
            onSuccessCallback,
            onErrorCallback,
            { title: 'Por favor espere...', text: 'Verificando...' }
        );
    }

    // Elimina el registro (llamado despuΓ©s de confirmar)
    EliminarTraza(idTraza, onSuccessCallback = null, onErrorCallback = null) {
        this.Call(
            'EliminarTraza',
            'post',
            { IdTraza: idTraza },
            onSuccessCallback,
            onErrorCallback,
            { title: 'Por favor espere...', text: 'Eliminando el registro' }
        );
    }

    // Carga el HTML del popup (nuevo o ediciΓ³n)
    PopUpTraza(idTraza, nombreIdPadre, valorIdPadre, onSuccessCallback = null, onErrorCallback = null) {
        this.Call(
            'PopUpTraza',
            'post',
            {
                IdTraza: idTraza,
                NombreIdPadre:  nombreIdPadre,
                ValorIdPadre:   valorIdPadre
            },
            onSuccessCallback,
            onErrorCallback
        );
    }

    // Guarda el registro (insert o update)
    GuardarTraza(entity, onSuccessCallback = null, onErrorCallback = null) {
        this.Call(
            'GuardarTraza',
            'post',
            entity,
            onSuccessCallback,
            onErrorCallback,
            { title: 'Por favor espere...', text: 'Guardando el registro' }
        );
    }

    // Busca registros y retorna el HTML de la grilla
    BuscarTraza(entity, onSuccessCallback = null, onErrorCallback = null) {
        this.Call(
            'BuscarTraza',
            'post',
            entity,
            onSuccessCallback,
            onErrorCallback
        );
    }
}

// Factory function usada internamente por el framework
function TrazaSvc_invoke() {
    return new TrazaSvc();
}

Signatura de this.Call()

ParΓ‘metroTipoDescripciΓ³n
1Β° β€” mΓ©todostringNombre del mΓ©todo en el Controller API (case-sensitive)
2Β° β€” verbo HTTPstringSiempre 'post' en este proyecto
3Β° β€” datosobjectPayload que recibirΓ‘ el Controller como parΓ‘metros
4Β° β€” onSuccessfunction|nullCallback al Γ©xito (result = dato retornado)
5Β° β€” onErrorfunction|nullCallback al error (errorCode, errorMessage)
6Β° β€” loadingMsgobject|nullMensaje del spinner mientras espera (opcional)

Trait JS β€” TrazaSvcT.js

public/services/mantenedores/Tools/TrazaSvcT.js
// CΓ³digo adicional para el funcionamiento del mantenedor de: Traza
// Agregar aquΓ­ mΓ©todos JS adicionales si se necesitan

JS LΓ³gica Frontend β€” traza.js

Este archivo contiene los event handlers y la lΓ³gica del frontend: quΓ© pasa cuando el usuario hace click en cada botΓ³n. El framework llama automΓ‘ticamente a estas funciones usando la convenciΓ³n {keyDelBoton}_OnClick.

UbicaciΓ³n del archivo

public/scripts/mantenedores/tools/traza.js

CΓ³digo completo

// ── INICIALIZACIΓ“N ───────────────────────────────────────────────
$(function() {
    // AquΓ­ puedes ejecutar cΓ³digo al cargar la pΓ‘gina
    // Ej: simular click en "Buscar" para mostrar todos al entrar
    // $('#btnBuscarTraza').trigger("click");
});

// ── VALIDACIΓ“N DE CAMPO INDIVIDUAL ──────────────────────────────
function ValidarTrazaElemento(elemento) {
    var errores = [];
    var empty   = false;
    var name    = $(elemento).attr('data-property-name');
    var label   = $(elemento).attr('data-property-label');

    if ($(elemento).attr('required') == 'required') {
        if ($(elemento).IsEmpty()) {
            errores.push("Este campo es obligatorio");
            empty = true;
        }
    }
    // Otras validaciones: RUT, email, telΓ©fono
    if (!empty && $(elemento).attr('validar-rut') != null) {
        if (!$(elemento).ValidarRut()) errores.push("El rut ingresado no es vΓ‘lido");
    }

    return { Field: name, Label: label, Cantidad: errores.length, Errores: errores };
}

// ── VALIDACIΓ“N GENERAL DEL FORMULARIO ───────────────────────────
function ValidarTrazaGeneral(eventInfo) {
    var result         = "";
    var erroresGenerales = [];
    var obj = Object.entries(eventInfo.FormElements);

    for (var i = 0; i < obj.length; i++) {
        let elemento = obj[i][1];
        let errores  = ValidarTrazaElemento(elemento);

        if (errores.Cantidad > 0) {
            erroresGenerales.push(errores);
            $(elemento).addClass('form-error');
        }
    }

    if (erroresGenerales.length > 0) {
        result = '<div class="errores-validacion-container">';
        erroresGenerales.forEach(function(field) {
            result += '<div class="error-validacion-container">';
            result += '<div class="error-validacion-titulo">' + field.Label + '</div>';
            field.Errores.forEach(function(msg) {
                result += '<div class="error-validacion-detalle">' + msg + '</div>';
            });
            result += '</div>';
        });
        result += '</div>';
    }
    return result;
}

// ── EVENTO: LIMPIAR FILTROS ──────────────────────────────────────
function btnLimpiarTraza_OnClick(eventInfo) {
    var obj = Object.entries(eventInfo.FormElements);
    for (var i = 0; i < obj.length; i++) {
        let elemento = obj[i][1];
        if ($(elemento).is('input') || $(elemento).prop('type') == 'text') {
            $(elemento).val('');
        }
        else if ($(elemento).prop('type') == 'select-one' || $(elemento).prop('type') == 'select-multiple') {
            $(elemento).val(-1).change();
        }
    }
}

// ── EVENTO: BUSCAR ───────────────────────────────────────────────
function btnBuscarTraza_OnClick(eventInfo) {

    // Mostrar spinner mientras carga
    $('#formulario-traza').html($('#content-wait-modal').html());
    if ($.fn.DataTable.isDataTable('#tbTraza')) {
        $('#tbTraza').DataTable().destroy();
    }

    Swal.fire({ title: 'Buscando', allowOutsideClick: false,
                showConfirmButton: false, willOpen: () => Swal.showLoading() });

    var formData = eventInfo.FormData;
    var entity = {
        RegistroTrazaBuscar: JSON.stringify({
            Descripcion: (formData.Descripcion == null) ? '' : formData.Descripcion,
        })
    };

    var trazaSvc = new TrazaSvc();
    trazaSvc.BuscarTraza(
        entity,
        function(result) {
            Swal.close();
            $('#formulario-traza').html(result);  ← inyecta la grilla
        },
        function(errorCode, errorMessage) {
            Swal.close();
            $('#formulario-traza').html('<div class="note-box">' + errorMessage + '</div>');
        }
    );
}

// ── FUNCIΓ“N AUXILIAR: abrir popup ───────────────────────────────
function PopUpTraza(idTraza = 0) {

    var nombreIdPadre = $('#nombre-id-padre').val() || "";
    var valorIdPadre  = $('#valor-id-padre').val()  || -1;

    const trazaPopUp = NewPopUp({ dismissOnOutsideClick: true });
    var trazaSvc = new TrazaSvc();

    trazaSvc.PopUpTraza(
        idTraza, nombreIdPadre, valorIdPadre,
        function(result) {
            $(trazaPopUp).RefreshPopUp(result);  ← inyecta el HTML del modal
        },
        function(errorCode, errorMessage) {
            Toast.fire({ icon: 'error', title: errorMessage });
            $('.modal').modal('hide');
        }
    );
}

// ── EVENTO: NUEVO ────────────────────────────────────────────────
function btnNuevaTraza_OnClick() {
    PopUpTraza();          ← sin ID β†’ modo nuevo
}

// ── EVENTO: EDITAR ───────────────────────────────────────────────
function btnEditarTraza_OnClick(rowInfo) {
    PopUpTraza(rowInfo.RowData.pk);  ← con ID β†’ modo ediciΓ³n
}

// ── EVENTO: GUARDAR (en el popup) ────────────────────────────────
function btnGuardarTraza_OnClick(eventInfo) {

    let formData = eventInfo.FormData;

    // 1. Validar campos requeridos
    var validacionGeneral = ValidarTrazaGeneral(eventInfo);
    if (validacionGeneral != "") {
        Swal.fire({ title: "Errores en el formulario", html: validacionGeneral,
                    width: 640, icon: 'error' });
        return;
    }

    // 2. ValidaciΓ³n personalizada (opcional, en trazaT.js)
    if (typeof ValidarTraza === 'function') {
        var validacion = ValidarTraza(formData);
        if (validacion != "") {
            Toast.fire({ icon: 'error', title: validacion });
            return;
        }
    }

    // 3. Armar el payload y llamar al servicio
    const trazaSvc = new TrazaSvc();
    var entity = {
        TrazaRegistro: JSON.stringify({
            IdTraza: formData.IdTraza,
            Descripcion:    formData.Descripcion,
            IndVigente:     formData.IndVigente,
            IndEliminado:   formData.IndEliminado,
        })
    };

    trazaSvc.GuardarTraza(
        entity,
        function(success) {
            // Refrescar la grilla
            $('#btnBuscarTraza').trigger("click");
            Swal.fire({
                title: 'Β‘Guardado!',
                text:  'El registro fue guardado correctamente.',
                icon:  "success",
                confirmButtonText: "OK",
            }).then(() => { $('.modal').last().modal('hide'); });
        },
        function(errorCode, errorMessage) {
            Toast.fire({ icon: 'error', title: errorMessage });
        }
    );
}

// ── EVENTO: ELIMINAR ────────────────────────────────────────────
function btnEliminarTraza_OnClick(rowInfo) {

    const trazaSvc = new TrazaSvc();
    var idTraza    = rowInfo.RowData.pk;

    // 1. Verificar si hay advertencias antes de mostrar confirmaciΓ³n
    trazaSvc.SePuedeEliminarTraza(
        idTraza,
        function(result) {

            var alertWidth = 400;
            if (result != "") {
                result     = '<span class="swal-warning">' + result + '</span><br><br>';
                alertWidth = 640;
            }

            // 2. Mostrar confirmaciΓ³n
            Swal.fire({
                title:              'ΒΏEliminar registro?',
                html:               result + '<span>ΒΏConfirmas la eliminaciΓ³n?</span>',
                showCancelButton:   true,
                width:              alertWidth,
                confirmButtonText:  'SΓ­, eliminar',
                cancelButtonText:   'Cancelar',
                confirmButtonColor: '#e0001c',
                cancelButtonColor:  '#9b9b9b',
            }).then((confirmed) => {
                if (confirmed.isConfirmed) {

                    // 3. Ejecutar eliminaciΓ³n
                    trazaSvc.EliminarTraza(
                        idTraza,
                        function() {
                            DropTableRow('tbTraza', idTraza);  ← quita la fila de la grilla
                            Toast.fire({ icon: 'info', title: 'Registro eliminado.' });
                        },
                        function(errorCode, errorMessage) {
                            Toast.fire({ icon: 'error', title: errorMessage });
                        }
                    );
                }
            });
        },
        function(errorCode, errorMessage) {
            Toast.fire({ icon: 'error', title: errorMessage });
        }
    );
}

ConvenciΓ³n de nombres de funciones

FunciΓ³n JSSe llama cuandoCΓ³mo lo sabe el framework
btnBuscarTraza_OnClick(eventInfo)Click en botΓ³n Key: 'btnBuscarTraza'Framework construye el nombre: {Key}_OnClick
btnNuevaTraza_OnClick()Click en botΓ³n Key: 'btnNuevaTraza'Mismo patrΓ³n
btnEditarTraza_OnClick(rowInfo)Click en botΓ³n de fila Key: 'btnEditarTraza'Recibe rowInfo.RowData con los atributos data de la fila
btnGuardarTraza_OnClick(eventInfo)Click en botΓ³n del formulario popupeventInfo.FormData tiene los valores del form
btnEliminarTraza_OnClick(rowInfo)Click en botΓ³n de fila eliminarRecibe rowInfo.RowData.pk con el ID del registro

Trait JS β€” trazaT.js

public/scripts/mantenedores/tools/trazaT.js
// Extensiones personalizadas del mantenedor de Traza
// Ejemplos de funciones que puedes definir aquΓ­:

// ValidaciΓ³n adicional al guardar:
// function ValidarTraza(formData) {
//     if (formData.Descripcion.length < 3) return "La descripciΓ³n debe tener al menos 3 caracteres.";
//     return "";
// }

// LΓ³gica post-refresco de grilla:
// function CustomRefrescarTrazas(IdTraza) {
//     return true; // retornar true ejecuta el refresco estΓ‘ndar
// }

JS Registro del Bundle JavaScript

Para que la vista pueda incluir los archivos JS del mantenedor con la directiva @@IncludeScriptBundle(mantenedorTrazaJS), ese bundle debe estar registrado en el archivo de configuraciΓ³n central.

UbicaciΓ³n del archivo

Application/Configuration/bundles.config.json

Entrada a agregar

Busca el archivo JSON y agrega la siguiente entrada en el objeto raΓ­z:

{
  // ... otras entradas existentes ...

  "mantenedorTrazaJS": {
    "IdBundle":       "mantenedorTrazaJS",
    "Tipo":           "javascript",
    "Versionado":     true,
    "BundleLocation": "scripts",
    "Sources": [
      { "Folder": "mantenedores/tools", "File": "traza.js"  },
      { "Folder": "mantenedores/tools", "File": "trazaT.js" }
    ]
  }
}

ΒΏQuΓ© hace cada campo?

CampoValorDescripciΓ³n
IdBundlemantenedorTrazaJSID ΓΊnico del bundle β€” debe coincidir con el nombre de la clave y con @@IncludeScriptBundle(...)
TipojavascriptIndica que son archivos JS
VersionadotrueEl framework agrega un hash al nombre para forzar cachΓ© bust al cambiar
BundleLocationscriptsCarpeta base dentro de public/scripts/
Sources[].Foldermantenedores/toolsSubcarpeta dentro de BundleLocation
Sources[].Filetraza.jsNombre del archivo
⚠️ Importante El ServiceJS bundle ya incluye todos los archivos *.js de la carpeta services/mantenedores/Tools/ con un wildcard, por lo que TrazaSvc.js ya estÑ incluido automÑticamente en el bundle global. Solo necesitas registrar los archivos de scripts/ (la lógica de eventos).

βœ… Checklist Final

Usa esta lista para verificar que no falta ningΓΊn archivo antes de probar el mantenedor en el navegador.

Capa DAO β€” 8 archivos

Capa BLL β€” 10 archivos

Controllers β€” 4 archivos

Vistas PHP β€” 4 archivos

JavaScript β€” 4 archivos

ConfiguraciΓ³n β€” 1 entrada en JSON

βœ… Total: 27 archivos Si todos estΓ‘n creados, el mantenedor deberΓ­a funcionar completamente. Navega a la URL del controller web para verificarlo.

Checklist de funcionamiento

πŸ“‹ Referencia RΓ‘pida β€” Convenciones de Nombres

ArtefactoPatrΓ³nEjemplo
Entity (tabla){Nombre}Traza
Entity (vista)VW{Nombre}VWTraza
DAO{Nombre}DaoTrazaDao
DTO{Nombre}DtoTrazaDto
Service BLL{Nombre}SvcTrazaSvc
Business Object{Nombre}BOTrazaBO
Trait de cualquiera{ClaseBase}TTrazaBOT
Controller Web{Nombre}ControllerTrazaController
Controller API{Nombre}Controller (distinto namespace)Api\Mantenedores\Tools\TrazaController
Vista principalverView.phpViews/Mantenedores/Tools/Traza/verView.php
Vista grillaverGridView.phpViews/Mantenedores/Tools/Traza/verGridView.php
Vista popuppopupView.phpViews/Mantenedores/Tools/Traza/popupView.php
Template grillaverGridT.phpViews/Mantenedores/Tools/Traza/verGridT.php
Svc JS{Nombre}Svc.jspublic/services/mantenedores/Tools/TrazaSvc.js
Script JS{nombre-minuscula}.jspublic/scripts/mantenedores/tools/traza.js
Bundle JSmantenedor{Nombre}JSmantenedorTrazaJS