Hola Charly,
Gracias por esta nueva opción que has implementado a Hix, y como que hemos pensando lo mismo de una manera diferente, pero basicamente, como tu dices, en un desarrollo web moderno, es necesario separar la logica de nuestra vista, para ir creciendo en el proyecto.
Yo lo hice de una manera diferente, y te explico como lo hice:
Yo tengo mi "Controlador" en un archivo "cliente.prg", con mi codigo Harbour, donde proceso los datos:
function ClientsPage()
return MemoRead( GetAppPath() + "mods/clien/clientes.view" )
function GetClientDetail(hParams)
local cId := iif( hb_HHasKey( hParams, 'id' ), hParams['id'], "" )
local hData := {=>}
local cQuery := "", hRow, aKeys, n
local oApi := GetApiConn( "clientes" )
// URL decode the ID to handle spaces and special chars
cId := StrTran( cId, "%20", " " )
// 1. Fetch main client record
cQuery := "SELECT * FROM clientes WHERE TRIM(id_codigo) = '" + AllTrim(cId) + "'"
if oApi:Open( cQuery ) .and. Len(oApi:aData) > 0
hData["client"] := oApi:aData[1]
else
hData["error"] := "Cliente no encontrado [" + AllTrim(cId) + "]"
if !Empty(oApi:cErrorMsg)
hData["error"] := oApi:cErrorMsg
endif
hData["query"] := cQuery
return hb_jsonEncode( hData )
endif
// 2. Fetch extended data (optional)
cQuery := "SELECT calle, casa, apto, zona, colonia, municipio, depto, postal, " + ;
"callec, casac, aptoc, zonac, coloniac, municipioc, deptoc, postalc, " + ;
"email2, email3, cargo, empresa, id_depto as id_depto_conta, id_seccion as id_seccion_conta " + ;
"FROM clientesdatadi WHERE TRIM(id_codigo) = '" + AllTrim(cId) + "'"
if oApi:Open( cQuery ) .and. Len(oApi:aData) > 0
hRow := oApi:aData[1]
aKeys := hb_HKeys( hRow )
for n := 1 to Len( aKeys )
hData["client"][ aKeys[n] ] := hRow[ aKeys[n] ]
next
endif
return hb_jsonEncode( ValToUtf8( hData ) )
Y la vista la tengo en un archivo "clientes.view" que basicamente es un archivo que contiene Html, CSS y JS o lo que se necesite para la vista:
<div class="page-wrapper">
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">Consulta de Clientes</h2>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{BASE}}sem.prg?action=dashboard">Inicio</a></li>
<li class="breadcrumb-item"><a href="#">Archivos</a></li>
<li class="breadcrumb-item active" aria-current="page">Clientes</li>
</ol>
</nav>
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="card-body">
<div id="table-loader" class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
<div class="mt-2">Cargando clientes...</div>
</div>
<div class="table-responsive d-none" id="table-container">
<table id="tblClientes" class="table card-table table-vcenter text-nowrap datatable">
<thead>
<tr>
<th>ID</th>
<th>NOMBRE</th>
<th>NIT</th>
<th>DIRECCIÓN</th>
<th>TELÉFONO</th>
<th class="text-end">SALDO</th>
<th class="text-center">OPCIONES</th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function() {
const apiConfig = {{API_CONFIG}};
const apiUrl = "{{API_URL}}";
$('#tblClientes').DataTable({
processing: true,
serverSide: false,
ajax: {
url: apiUrl,
type: 'POST',
contentType: 'application/json',
data: function(d) {
return JSON.stringify({
...apiConfig,
query: "SELECT id_codigo, cliente, nit, direccion, telefono, saldo FROM clientes ORDER BY cliente ASC"
});
},
dataSrc: function(json) {
$('#table-loader').addClass('d-none');
$('#table-container').removeClass('d-none');
return json.data || [];
},
error: function(xhr, error, thrown) {
$('#table-loader').html('<div class="alert alert-danger">Error al cargar clientes: ' + (xhr.responseJSON ? xhr.responseJSON.error : thrown) + '</div>');
}
},
columns: [
{ data: 'id_codigo', className: 'fw-bold' },
{ data: 'cliente' },
{ data: 'nit' },
{
data: 'direccion',
render: function(data) {
return `<span class="text-truncate d-inline-block" style="max-width: 250px;" title="${data}">${data}</span>`;
}
},
{ data: 'telefono' },
{
data: 'saldo',
className: 'text-end fw-bold',
render: function(data) {
return 'Q. ' + parseFloat(data || 0).toLocaleString('en-US', {minimumFractionDigits: 2});
}
},
{
data: null,
className: 'text-center',
orderable: false,
render: function(data, type, row) {
return `
<div class="btn-list flex-nowrap justify-content-center">
<a href="javascript:void(0)" class="btn btn-icon btn-outline-primary rounded-circle" title="Ver Detalle" onclick="showClientDetail('${row.id_codigo}')">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><circle cx="12" cy="12" r="2" /><path d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 -2.333 10 7" /></svg>
</a>
</div>
`;
}
}
],
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/es-ES.json'
},
pageLength: 15,
dom: "<'row mb-3'<'col-sm-12'f>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row mt-3'<'col-sm-5'i><'col-sm-7'p>>",
});
});
async function showClientDetail(id) {
try {
const res = await fetch(`{{BASE}}sem.prg?action=client_detail&id=${id}`);
const json = await res.json();
if (json.error) {
if (json.query) console.log("Debug Query:", json.query);
alert(json.error);
return;
}
const c = json.client;
// Header
document.getElementById('view-cliente-nombre').innerText = c.cliente;
document.getElementById('view-client-codigo').innerText = c.id_codigo;
// Generales
document.getElementById('view-codweb').innerText = c.codweb || '--';
document.getElementById('view-status').innerText = c.status === 'A' ? 'Alta' : (c.status === 'B' ? 'Baja' : c.status);
document.getElementById('view-fechain').innerText = c.fechain ? new Date(c.fechain).toLocaleDateString('es-GT') : '--';
document.getElementById('view-fechauv').innerText = c.fechauv ? new Date(c.fechauv).toLocaleDateString('es-GT') : '--';
document.getElementById('view-facturar').innerText = c.facturar || c.cliente;
document.getElementById('view-nit').innerText = c.nit || '--';
document.getElementById('view-cui').innerText = c.cui || '--';
document.getElementById('view-clasifica').innerText = c.clasifica || '--';
document.getElementById('view-atencion').innerText = c.atencion || '--';
document.getElementById('view-region').innerText = c.region || '--';
document.getElementById('view-telefono').innerText = c.telefono || '--';
document.getElementById('view-celular').innerText = c.celular || '--';
document.getElementById('view-fax').innerText = c.fax || '--';
document.getElementById('view-email').innerText = c.email || '--';
document.getElementById('view-email2').innerText = c.email2 || '--';
// Otros
document.getElementById('view-comosupo').innerText = c.comosupo || '--';
document.getElementById('view-fechana').innerText = c.fechana ? new Date(c.fechana).toLocaleDateString('es-GT') : '--';
document.getElementById('view-calle').innerText = c.calle || '--';
document.getElementById('view-casa').innerText = c.casa || '--';
document.getElementById('view-zona').innerText = c.zona || '--';
document.getElementById('view-apto').innerText = c.apto || '--';
document.getElementById('view-colonia').innerText = c.colonia || '--';
document.getElementById('view-postal').innerText = c.postal || '--';
document.getElementById('view-depto').innerText = c.depto || '--';
document.getElementById('view-municipio').innerText = c.municipio || '--';
document.getElementById('view-obs').innerText = c.obs || '--';
document.getElementById('view-depto-conta').innerText = c.id_depto_conta || '--';
document.getElementById('view-seccion-conta').innerText = c.id_seccion_conta || '--';
const modal = new bootstrap.Modal(document.getElementById('modal-client-detail'));
modal.show();
} catch (e) {
console.error("Error cargando detalle de cliente:", e);
alert("Ocurrió un error al cargar el detalle.");
}
}
</script>
<!-- Modal de Detalle de Cliente -->
<div class="modal modal-blur fade" id="modal-client-detail" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Detalle de Cliente: <span id="view-cliente-nombre"></span></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<div class="card border-0">
<ul class="nav nav-tabs nav-fill" data-bs-toggle="tabs">
<li class="nav-item">
<a href="#tab-generales" class="nav-link active" data-bs-toggle="tab">Generales</a>
</li>
<li class="nav-item">
<a href="#tab-otros" class="nav-link" data-bs-toggle="tab">Otros</a>
</li>
</ul>
<div class="card-body">
<div class="tab-content">
<!-- Tab 1: Generales -->
<div class="tab-pane active show" id="tab-generales">
<div class="row g-3">
<div class="col-md-3">
<small class="text-secondary fw-bold text-uppercase">Código</small>
<div class="fw-bold text-dark h4" id="view-client-codigo">--</div>
</div>
<div class="col-md-3">
<small class="text-secondary fw-bold text-uppercase">Usuario Web</small>
<div class="fw-bold text-dark" id="view-codweb">--</div>
</div>
<div class="col-md-2">
<small class="text-secondary fw-bold text-uppercase">Estatus</small>
<div class="fw-bold text-dark" id="view-status">--</div>
</div>
<div class="col-md-2">
<small class="text-secondary fw-bold text-uppercase">Fecha Alta</small>
<div class="fw-bold text-dark" id="view-fechain">--</div>
</div>
<div class="col-md-2">
<small class="text-secondary fw-bold text-uppercase">Ult. Venta</small>
<div class="fw-bold text-dark" id="view-fechauv">--</div>
</div>
<div class="col-12 mt-3">
<div class="bg-blue-lt p-2 rounded">
<small class="text-primary fw-bold text-uppercase">Nombre / Factura</small>
<div class="h3 mb-0 text-dark" id="view-facturar">--</div>
</div>
</div>
<div class="col-md-4">
<small class="text-secondary fw-bold text-uppercase">NIT</small>
<div class="fw-bold text-dark" id="view-nit">--</div>
</div>
<div class="col-md-4">
<small class="text-secondary fw-bold text-uppercase">DPI / CUI</small>
<div class="fw-bold text-dark" id="view-cui">--</div>
</div>
<div class="col-md-4">
<small class="text-secondary fw-bold text-uppercase">Clasificación</small>
<div class="fw-bold text-dark" id="view-clasifica">--</div>
</div>
<div class="col-md-8">
<small class="text-secondary fw-bold text-uppercase">Atención</small>
<div class="fw-bold text-dark" id="view-atencion">--</div>
</div>
<div class="col-md-4">
<small class="text-secondary fw-bold text-uppercase">Región</small>
<div class="fw-bold text-dark" id="view-region">--</div>
</div>
<div class="col-md-4 mt-3">
<small class="text-secondary fw-bold text-uppercase">Teléfono</small>
<div class="fw-bold text-dark" id="view-telefono">--</div>
</div>
<div class="col-md-4 mt-3">
<small class="text-secondary fw-bold text-uppercase">Celular</small>
<div class="fw-bold text-dark" id="view-celular">--</div>
</div>
<div class="col-md-4 mt-3">
<small class="text-secondary fw-bold text-uppercase">Fax/Telf 2</small>
<div class="fw-bold text-dark" id="view-fax">--</div>
</div>
<div class="col-md-6 mt-3">
<small class="text-secondary fw-bold text-uppercase">Correo 1</small>
<div class="fw-bold text-dark" id="view-email">--</div>
</div>
<div class="col-md-6 mt-3">
<small class="text-secondary fw-bold text-uppercase">Correo 2</small>
<div class="fw-bold text-dark" id="view-email2">--</div>
</div>
</div>
</div>
<!-- Tab 2: Otros -->
<div class="tab-pane" id="tab-otros">
<div class="row g-3">
<div class="col-md-6">
<small class="text-secondary fw-bold text-uppercase">Cómo supo de nosotros</small>
<div class="fw-bold text-dark" id="view-comosupo">--</div>
</div>
<div class="col-md-6">
<small class="text-secondary fw-bold text-uppercase">Fecha Nacimiento</small>
<div class="fw-bold text-dark" id="view-fechana">--</div>
</div>
<div class="col-md-6 mt-3">
<small class="text-secondary fw-bold text-uppercase">Calle / Avenida</small>
<div class="fw-bold text-dark" id="view-calle">--</div>
</div>
<div class="col-md-3 mt-3">
<small class="text-secondary fw-bold text-uppercase">No. Casa</small>
<div class="fw-bold text-dark" id="view-casa">--</div>
</div>
<div class="col-md-3 mt-3">
<small class="text-secondary fw-bold text-uppercase">Zona</small>
<div class="fw-bold text-dark" id="view-zona">--</div>
</div>
<div class="col-md-4 mt-3">
<small class="text-secondary fw-bold text-uppercase">No. Apartamento</small>
<div class="fw-bold text-dark" id="view-apto">--</div>
</div>
<div class="col-md-5 mt-3">
<small class="text-secondary fw-bold text-uppercase">Colonia / Barrio</small>
<div class="fw-bold text-dark" id="view-colonia">--</div>
</div>
<div class="col-md-3 mt-3">
<small class="text-secondary fw-bold text-uppercase">Apartado Postal</small>
<div class="fw-bold text-dark" id="view-postal">--</div>
</div>
<div class="col-md-6 mt-3">
<small class="text-secondary fw-bold text-uppercase">Departamento</small>
<div class="fw-bold text-dark" id="view-depto">--</div>
</div>
<div class="col-md-6 mt-3">
<small class="text-secondary fw-bold text-uppercase">Municipio</small>
<div class="fw-bold text-dark" id="view-municipio">--</div>
</div>
<div class="col-12 mt-3">
<small class="text-secondary fw-bold text-uppercase">Observaciones</small>
<div class="p-2 border rounded bg-light" id="view-obs" style="min-height: 60px;">--</div>
</div>
<div class="col-12 mt-4">
<div class="hr-text">Centro de Costo</div>
<div class="row">
<div class="col-md-6">
<small class="text-secondary fw-bold text-uppercase">Depto Contable</small>
<div class="fw-bold text-dark" id="view-depto-conta">--</div>
</div>
<div class="col-md-6">
<small class="text-secondary fw-bold text-uppercase">Sección Contable</small>
<div class="fw-bold text-dark" id="view-seccion-conta">--</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer bg-light p-2">
<button type="button" class="btn btn-outline-secondary ms-auto" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>

Pero ahora con esta nueva opción de "Vistas" de Hix, puedo incluir codigo Harbour dentro de la vista, pasandole parametros, lo cual me da mas opciones para seguir manteniendo mi desarrollo mas amigable a lo que he hecho por años.
Nota: Toda la idea anterior, la he conseguido desarrollar y hacer funcionar con la ayuda de Antigravity, usando los agentes de Gemini y Claude, y tu servidor Hix los trabaja de manera optima y rápida.
Saludos cordiales.
Carlos.