Appearance
Arquitectura Frontend Legacy
Estado: ⚠️ LEGACY - Solo mantenimiento
ARQUITECTURA DEPRECADA
Esta arquitectura está obsoleta y no debe usarse para nuevos desarrollos. Solo se mantiene para dar soporte al código existente. Para nuevas features, consulte Arquitectura Moderna React.
1. Visión General
Contexto Histórico
El frontend legacy de Sistema Bautista se construyó con PHP Server-Side Rendering (SSR) y JavaScript Vanilla antes de la migración a React. Esta arquitectura se mantiene activa para:
- ✅ Mantenimiento de módulos existentes
- ✅ Corrección de bugs en features legacy
- ❌ NO para nuevos desarrollos
Características Principales
- Views PHP: Renderizado en servidor con lógica de negocio
- JavaScript Vanilla: Interactividad y comunicación con backend
- Modales dinámicos: Cargados con
fetch()y montados en DOM - API directa: Comunicación directa con backend (sin proxy)
- jQuery: Framework DOM para manipulación y eventos
- Bootstrap 4: Framework CSS para estilos y componentes
Stack Tecnológico
| Componente | Tecnología | Versión |
|---|---|---|
| Server-Side | PHP | 8.2+ |
| Client-Side | JavaScript Vanilla | ES6+ |
| DOM Manipulation | jQuery | 3.x |
| CSS Framework | Bootstrap | 4.x |
| HTTP Requests | Fetch API | Nativo |
2. Estructura de Archivos
Paths Completos desde Root
Todos los paths se especifican desde la raíz del repositorio bautista-app/:
bautista-app/
├── php/
│ ├── components/ # Views PHP (Server-Side Rendering)
│ │ ├── mod-ventas/ # Módulo de ventas
│ │ │ ├── forms/ # Formularios de ventas
│ │ │ │ └── tarjeta.php # View de tarjetas
│ │ │ └── modals/ # Modales del módulo
│ │ ├── mod-compras/ # Módulo de compras
│ │ │ ├── modals/ # Modales del módulo
│ │ │ │ ├── modal-item-subdiario.php # Modal de items
│ │ │ │ ├── modal-conceptos-subdiario.php # Modal de conceptos
│ │ │ │ └── ...
│ │ │ └── main-sidebar-compras.php
│ │ ├── mod-stock/ # Módulo de stock
│ │ ├── mod-ctacte/ # Módulo de cuenta corriente
│ │ ├── mod-tesoreria/ # Módulo de tesorería
│ │ └── mod-crm/ # Módulo de CRM
│ │
│ └── backend/ # ⚠️ PROXY DEPRECADO (NO USAR)
│ ├── cliente.php # Proxy de cliente
│ ├── proveedor.php # Proxy de proveedor
│ └── ...
│
├── js/
│ ├── view/ # JavaScript de interactividad
│ │ ├── mod-ventas/ # JS de ventas
│ │ │ ├── tarjeta.js # Interactividad de tarjetas
│ │ │ ├── sidebar.js # Interactividad sidebar
│ │ │ └── ...
│ │ ├── mod-compras/ # JS de compras
│ │ │ ├── subdiario-compras/
│ │ │ │ ├── index.js # Vista principal
│ │ │ │ ├── form-items.js # Formulario de items
│ │ │ │ ├── list-conceptos.js # Lista de conceptos
│ │ │ │ └── ...
│ │ │ └── ...
│ │ ├── mod-stock/ # JS de stock
│ │ ├── mod-ctacte/ # JS de cuenta corriente
│ │ ├── mod-tesoreria/ # JS de tesorería
│ │ └── mod-crm/ # JS de CRM
│ │
│ ├── middleware/ # Middleware de API
│ │ └── API.js # ApiRequest class (legacy)
│ │
│ ├── util/ # Utilidades compartidas
│ │ ├── export-methods.js # Métodos de utilidad
│ │ ├── inputs.js # Helpers de inputs
│ │ └── ...
│ │
│ └── constants/ # Constantes del sistema
│ └── general/
│ └── Periodo.js # Constantes de período
│
└── view/ # Templates PHP principales
├── mod-ventas/ # Views de ventas
├── mod-compras/ # Views de compras
└── ...Convención de Naming
| Tipo | Pattern | Ejemplo |
|---|---|---|
| View PHP | {nombre}.php | tarjeta.php |
| Modal PHP | modal-{nombre}.php | modal-item-subdiario.php |
| Modal HTML | modal-{nombre}.html | modal-subdiario-venta.html |
| JavaScript View | {nombre}.js | tarjeta.js |
| JavaScript Modal | form-{nombre}.js o list-{nombre}.js | form-items.js |
3. Patrones de Implementación
3.1. View PHP Base + JavaScript
Patrón: View PHP renderiza estructura HTML, JavaScript agrega interactividad.
Ubicación:
- PHP:
bautista-app/php/components/mod-{modulo}/forms/{nombre}.php - JS:
bautista-app/js/view/mod-{modulo}/{nombre}.js
Ejemplo completo:
View PHP: bautista-app/php/components/mod-venta/forms/tarjeta.php
php
<?php
session_start();
require_once __DIR__ . '/../../../vendor/autoload.php';
use App\Constants\Modulo;
$modulos = json_decode($_SESSION["SD_PERMISO_SISTEMA"], true);
$hasTesoreria = (bool)$modulos[Modulo::TESORERIA->value];
?>
<div class="modal fade" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 id="modalTitle" class="modal-title"></h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" tabindex="-1">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="card-body">
<form>
<div class="form-row">
<div class="form-group col-md-12">
<label for="inputNombre">Nombre</label>
<input name="nombre" type="text" class="form-control"
id="inputNombre" placeholder="Ingrese Nombre"
max="100" required>
</div>
</div>
<?php if ($hasTesoreria): ?>
<div class="form-row">
<div class="form-group col-md-12">
<label for="inputCuenta">Cuenta</label>
<span class="badge badge-info">Búsqueda por Código o nombre</span>
<input id="inputCuenta" type="text" class="form-control"
placeholder="Ingrese y seleccione la cuenta" required />
</div>
</div>
<?php endif; ?>
<div class="d-flex justify-content-around">
<button type="button" data-dismiss="modal" aria-label="Close"
class="btn btn-danger">
<i class="fa-solid fa-arrow-right-from-bracket"></i>Cancelar
</button>
<button id="btnRegistrarCliente" type="submit"
class="btn btn-primary">Aceptar</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>JavaScript: bautista-app/js/view/mod-venta/tarjeta.js
javascript
import { ApiRequest } from '../../middleware/API.js';
import { setAutocomplete } from '../../util/inputs.js';
import { isValidForm } from '../../util/form-methods.js';
// Referencias a elementos DOM
const modalTarjeta = $('#modalTarjeta');
const inputNombre = document.getElementById('inputNombre');
const inputCuenta = document.getElementById('inputCuenta');
const btnRegistrar = document.getElementById('btnRegistrarCliente');
const request = new ApiRequest();
// Objeto de datos
const tarjeta = {
id: null,
nombre: null,
cuenta: null,
reset() {
this.id = null;
this.nombre = null;
this.cuenta = null;
}
};
// Configuración de autocomplete para cuenta
if (inputCuenta) {
setAutocomplete(inputCuenta, {
url: `${URL_BACKEND}/cuentas`,
displayField: 'nombre',
valueField: 'id',
onSelect: (record) => {
tarjeta.cuenta = record;
}
});
}
// Evento de submit
btnRegistrar.addEventListener('click', async (e) => {
e.preventDefault();
if (!isValidForm(e.target.form)) return;
tarjeta.nombre = inputNombre.value;
try {
// ✅ CORRECTO: Fetch directo al backend API
const response = await request.post(
`${URL_BACKEND}/tarjetas`,
tarjeta
);
// Mostrar mensaje de éxito
showSuccessMessage('Tarjeta registrada correctamente');
// Cerrar modal
modalTarjeta.modal('hide');
// Recargar tabla
tablaTarjetas.ajax.reload();
} catch (error) {
console.error('Error al registrar tarjeta:', error);
showErrorMessage(error.message);
}
});
// Limpiar formulario al cerrar modal
modalTarjeta.on('hidden.bs.modal', () => {
tarjeta.reset();
inputNombre.value = '';
if (inputCuenta) inputCuenta.value = '';
});Características del patrón:
- ✅ View PHP renderiza estructura HTML
- ✅ JavaScript importa dependencias con ES6 modules
- ✅ Referencias a DOM con
getElementByIdo jQuery - ✅ Objeto de datos con métodos
reset(),getData(), etc. - ✅ Event listeners para interactividad
- ✅ Fetch directo a backend API (sin proxy)
3.2. Modal Legacy con Fetch Dinámico
Patrón: Modal cargado dinámicamente con fetch() y montado en DOM.
Ubicación:
- HTML/PHP:
bautista-app/php/components/mod-{modulo}/modals/modal-{nombre}.php - JS:
bautista-app/js/view/mod-{modulo}/{nombre}.jso módulo dedicado
Ejemplo completo:
Modal HTML: bautista-app/php/components/mod-compras/modals/modal-item-subdiario.php
php
<div class="modal fade" id="idModalDetalle" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Carga de item</h4>
<button type="button" class="close" data-dismiss="modal"
aria-label="Close" tabindex="-1">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form id="idFormDetalle">
<div class="form-row">
<div class="form-group col-md-12">
<label for="idInputProducto">Artículo</label>
<span class="badge badge-info">Búsqueda por código o descripción</span>
<input name="producto" type="text" class="form-control"
id="idInputProducto"
placeholder="Ingrese y seleccione artículo" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="idInputCantidad">Cantidad</label>
<input name="cantidad" type="number" value="1"
min="0.001" step="0.001" class="form-control"
id="idInputCantidad" required>
</div>
<div class="form-group col-md-6">
<label for="idInputPrecio">Precio Unitario</label>
<input name="precio" data-badge-type="F" type="text"
class="form-control" id="idInputPrecio" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="idInputBonificacion">Bonificación</label>
<input name="bonificacion" data-badge-type="P" type="text"
class="form-control" id="idInputBonificacion">
</div>
<div class="form-group col-md-6">
<label for="idInputImporte">Importe</label>
<input name="importe" type="text" data-badge-type="F"
class="form-control" id="idInputImporte">
</div>
</div>
<div class="form-group col-md-12">
<div class="form-group custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input"
id="idCheckboxActualizaCosto" checked>
<label for="idCheckboxActualizaCosto"
class="custom-control-label">Actualiza Costo</label>
</div>
</div>
<div class="d-flex justify-content-around">
<button type="button" data-dismiss="modal" aria-label="Close"
class="btn btn-danger">
<i class="fa-solid fa-arrow-right-from-bracket"></i>Cancelar
</button>
<button id="btnAgregarItem" type="submit"
class="btn btn-primary">Aceptar</button>
</div>
</form>
</div>
</div>
</div>
</div>JavaScript de carga: bautista-app/js/view/mod-ventas/sidebar.js
javascript
import { URL_APP } from '../../constants.js';
async function cargarModalSubdiario() {
try {
// ✅ CORRECTO: Fetch del modal desde componentes
const response = await fetch(
`${URL_APP}/php/components/mod-venta/modal-subdiario-venta.html`
);
if (!response.ok) {
throw new Error('No se pudo cargar el modal');
}
const formularioHTML = await response.text();
// Crear contenedor y agregar al DOM
const contenedor = document.createElement('div');
contenedor.innerHTML = formularioHTML;
document.getElementsByTagName('body')[0].appendChild(contenedor);
// Obtener referencias al modal
const modal = contenedor.querySelector('#modal');
const title = modal.querySelector('#modalHeader');
// Configurar modal
title.textContent = 'Nuevo Subdiario de Venta';
// Mostrar modal
$(modal).modal('show');
// Importar e inicializar lógica del modal
const { initModalSubdiario } = await import('./modals/subdiario-venta.js');
initModalSubdiario(modal);
} catch (error) {
console.error('Error al cargar modal:', error);
showErrorMessage('No se pudo cargar el formulario');
}
}
// Evento del botón que abre el modal
document.getElementById('btnNuevoSubdiario').addEventListener('click', cargarModalSubdiario);JavaScript de lógica: bautista-app/js/view/mod-compras/subdiario-compras/form-items.js
javascript
import { ApiRequest } from '../../../middleware/API.js';
import { multiplicar, restar, dividir, generateRandomString } from '../../../util/export-methods.js';
import { isValidForm } from '../../../util/form-methods.js';
import { setAutocomplete, setInputBadge } from '../../../util/inputs.js';
const formItems = (() => {
// Referencias Modal
const modalDetalle = $('#idModalDetalle');
const inputProducto = document.getElementById('idInputProducto');
const inputCantidad = document.getElementById('idInputCantidad');
const inputPrecio = document.getElementById('idInputPrecio');
const inputBonificacion = document.getElementById('idInputBonificacion');
const inputImporte = document.getElementById('idInputImporte');
const checkboxCosto = document.getElementById('idCheckboxActualizaCosto');
const formDetalle = document.getElementById('idFormDetalle');
const request = new ApiRequest();
// Objeto de datos con cálculo reactivo
const item = {
_bonificacion: null,
_cantidad: 1,
_precio: null,
unique: null,
id: null,
nombre: null,
importe: null,
actualiza_costo: true,
set precio(value) {
this._precio = value;
this.calculate();
},
get precio() {
return this._precio;
},
set cantidad(value) {
this._cantidad = value;
this.calculate();
},
get cantidad() {
return this._cantidad;
},
set bonificacion(value) {
this._bonificacion = value;
this.calculate();
},
get bonificacion() {
return this._bonificacion;
},
calculate() {
const cantidad = this.cantidad ?? 1;
const precio = this._precio ?? 0;
const bonificacion = this.bonificacion ?? 0;
// Cálculo: cantidad * precio * (1 - bonificacion/100)
this.importe = multiplicar(
cantidad,
multiplicar(precio, restar(1, dividir(bonificacion, 100)))
);
inputImporte.value = this.importe;
},
reset() {
this.unique = null;
this.id = null;
this._precio = null;
this.bonificacion = null;
this.importe = null;
this.cantidad = 1;
inputProducto.disabled = false;
formDetalle.reset();
},
getData() {
const datos = {};
Object.keys(this).forEach((key) => {
if (typeof this[key] !== 'function') {
datos[key] = this[key];
}
});
return datos;
}
};
// Configurar autocomplete de producto
setAutocomplete(inputProducto, {
url: `${URL_BACKEND}/productos`,
displayField: 'nombre',
valueField: 'id',
onSelect: (record) => {
item.id = record.id;
item.nombre = record.nombre;
item.precio = record.precio;
}
});
// Configurar badges de moneda
setInputBadge(inputPrecio);
setInputBadge(inputBonificacion);
setInputBadge(inputImporte);
// Eventos de cambio para cálculo reactivo
inputCantidad.addEventListener('input', () => {
item.cantidad = parseFloat(inputCantidad.value);
});
inputPrecio.addEventListener('input', () => {
item.precio = parseFloat(inputPrecio.value);
});
inputBonificacion.addEventListener('input', () => {
item.bonificacion = parseFloat(inputBonificacion.value);
});
// Evento de submit
formDetalle.addEventListener('submit', async (e) => {
e.preventDefault();
if (!isValidForm(formDetalle)) return;
item.actualiza_costo = checkboxCosto.checked;
item.unique = generateRandomString(10);
try {
// Agregar item a la tabla
agregarItemATabla(item.getData());
// Cerrar modal
modalDetalle.modal('hide');
showSuccessMessage('Item agregado correctamente');
} catch (error) {
console.error('Error al agregar item:', error);
showErrorMessage(error.message);
}
});
// Limpiar al cerrar modal
modalDetalle.on('hidden.bs.modal', () => {
item.reset();
});
// API pública
return {
open: () => modalDetalle.modal('show'),
edit: (data) => {
item.loadFrom(data);
modalDetalle.modal('show');
}
};
})();
export default formItems;Características del patrón:
- ✅ Modal cargado con
fetch()desde/php/components/ - ✅ HTML insertado dinámicamente en
<body> - ✅ JavaScript modular con IIFE o export
- ✅ Objeto de datos con getters/setters reactivos
- ✅ Autocomplete configurado con
setAutocomplete() - ✅ Validación con
isValidForm() - ✅ Event listeners para lógica de negocio
3.3. Comunicación con Backend
❌ INCORRECTO: Proxy Deprecado (NO USAR)
javascript
// ❌ NO USAR - Proxy php/backend/ DEPRECADO por rendimiento
import { ApiRequest } from '../../middleware/API.js';
const request = new ApiRequest();
// DEPRECADO: Proxy en bautista-app/php/backend/
const response = await request.get('/php/backend/cliente.php', {
id: 123
});Problemas del proxy:
- ⚠️ Rendimiento: Doble salto HTTP (frontend → proxy → backend)
- ⚠️ Mantenimiento: Duplicación de lógica de validación
- ⚠️ Escalabilidad: No soporta caching efectivo
- ⚠️ Debugging: Stack traces complejos
Estructura del proxy (solo referencia):
bautista-app/php/backend/
├── cliente.php # Proxy de cliente
├── proveedor.php # Proxy de proveedor
├── producto.php # Proxy de producto
└── ...Ejemplo de proxy legacy (bautista-app/php/backend/cliente.php):
php
<?php
session_start();
require_once('../constants.php');
require_once('../util/methods.php');
require_once('../service/Request.php');
header('content-type: application/json; charset=utf-8');
try {
if (!isset($_SESSION['SD_USER'])) {
throw new MissingSession();
}
// Switch manual de HTTP methods
switch ($_SERVER['REQUEST_METHOD']) {
case 'GET':
$data = [];
if (isset($_GET['id'])) {
$data['id'] = json_decode($_GET['id']);
}
if (isset($_GET['prueba'])) {
$data['prueba'] = filter_var($_GET['prueba'], FILTER_VALIDATE_BOOLEAN);
}
// ⚠️ Llamada al backend real
$request = new Request();
$response = $request->get(URL_BACKEND . '/clientes', $data);
echo json_encode($response);
break;
case 'POST':
// ... similar para POST
break;
case 'PUT':
// ... similar para PUT
break;
case 'DELETE':
// ... similar para DELETE
break;
default:
throw new Exception('Método HTTP no soportado');
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}✅ CORRECTO: Fetch Directo al Backend API
javascript
// ✅ USAR: Fetch directo a backend API (bautista-backend)
import { ApiRequest } from '../../middleware/API.js';
const request = new ApiRequest();
// Fetch directo al backend en URL_BACKEND
const response = await request.get(`${URL_BACKEND}/clientes`, {
id: 123,
prueba: false
});Ventajas del fetch directo:
- ✅ Rendimiento: Comunicación directa sin saltos
- ✅ Consistencia: Validación centralizada en backend
- ✅ Escalabilidad: Caching efectivo en backend
- ✅ Debugging: Stack traces claros
Clase ApiRequest legacy (bautista-app/js/middleware/API.js):
javascript
export class ApiRequest {
constructor() {
this.baseURL = URL_BACKEND; // Configurado en constants.js
this.headers = {
'Content-Type': 'application/json',
'X-Schema': this.getSchema() // Multi-tenant header
};
}
getSchema() {
// Obtener schema del localStorage
return localStorage.getItem('schema') || 'suc0001';
}
async get(url, params = {}) {
const queryString = new URLSearchParams(params).toString();
const fullURL = `${this.baseURL}${url}?${queryString}`;
const response = await fetch(fullURL, {
method: 'GET',
headers: this.headers
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
async post(url, body = {}) {
const response = await fetch(`${this.baseURL}${url}`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
async put(url, body = {}) {
const response = await fetch(`${this.baseURL}${url}`, {
method: 'PUT',
headers: this.headers,
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
async delete(url, body = {}) {
const response = await fetch(`${this.baseURL}${url}`, {
method: 'DELETE',
headers: this.headers,
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
}Uso completo:
javascript
import { ApiRequest } from '../../middleware/API.js';
import { URL_BACKEND } from '../../constants.js';
const request = new ApiRequest();
// GET con parámetros
try {
const clientes = await request.get(`${URL_BACKEND}/clientes`, {
filter: 'Juan',
prueba: false
});
console.log('Clientes:', clientes);
} catch (error) {
console.error('Error al obtener clientes:', error);
showErrorMessage(error.message);
}
// POST de nuevo registro
try {
const nuevoCliente = await request.post(`${URL_BACKEND}/clientes`, {
nombre: 'Juan Pérez',
cuit: '20-12345678-9',
email: 'juan@example.com'
});
showSuccessMessage('Cliente registrado correctamente');
} catch (error) {
console.error('Error al registrar cliente:', error);
showErrorMessage(error.message);
}
// PUT para modificación
try {
const clienteActualizado = await request.put(`${URL_BACKEND}/clientes/123`, {
nombre: 'Juan Pérez Actualizado',
email: 'juan.nuevo@example.com'
});
showSuccessMessage('Cliente actualizado correctamente');
} catch (error) {
console.error('Error al actualizar cliente:', error);
showErrorMessage(error.message);
}
// DELETE
try {
await request.delete(`${URL_BACKEND}/clientes/123`);
showSuccessMessage('Cliente eliminado correctamente');
} catch (error) {
console.error('Error al eliminar cliente:', error);
showErrorMessage(error.message);
}3.4. Helpers y Utilidades Legacy
Inputs y Autocomplete
javascript
import { setAutocomplete, setInputBadge, setInputFecha } from '../../util/inputs.js';
// Autocomplete
setAutocomplete(inputCliente, {
url: `${URL_BACKEND}/clientes`,
displayField: 'nombre',
valueField: 'id',
minChars: 2,
onSelect: (record) => {
console.log('Cliente seleccionado:', record);
}
});
// Badge de moneda ($ o %)
setInputBadge(inputImporte, { type: 'F' }); // F = Fijo ($)
setInputBadge(inputPorcentaje, { type: 'P' }); // P = Porcentaje (%)
// Input de fecha con datepicker
setInputFecha(inputFecha, {
format: 'dd/mm/yyyy',
autoclose: true
});Validación de Formularios
javascript
import { isValidForm } from '../../util/form-methods.js';
form.addEventListener('submit', (e) => {
e.preventDefault();
// Validación HTML5 + custom
if (!isValidForm(form)) {
showErrorMessage('Complete todos los campos requeridos');
return;
}
// Procesar formulario
submitForm();
});Utilidades Matemáticas
javascript
import {
multiplicar,
dividir,
sumar,
restar,
redondear
} from '../../util/export-methods.js';
// Evitar problemas de precisión de punto flotante
const importe = multiplicar(cantidad, precio);
const importeConDescuento = multiplicar(
importe,
restar(1, dividir(descuento, 100))
);
const importeRedondeado = redondear(importeConDescuento, 2);DataTables (Tablas)
javascript
import { LANG_TABLE } from '../../util/constantes.js';
import { reemplazarFila } from '../../util/datatable-methods.js';
// Inicialización de tabla
const tablaClientes = $('#tablaClientes').DataTable({
language: LANG_TABLE,
ajax: {
url: `${URL_BACKEND}/clientes`,
dataSrc: 'data'
},
columns: [
{ data: 'id' },
{ data: 'nombre' },
{ data: 'cuit' },
{ data: 'email' }
]
});
// Reemplazar fila después de edición
reemplazarFila(tablaClientes, clienteActualizado, 'id');
// Recargar tabla
tablaClientes.ajax.reload();4. Problemas de Arquitectura
4.1. Separación de Responsabilidades
Problema: View PHP mezcla lógica de presentación con lógica de negocio.
php
<?php
// ❌ Lógica de negocio en view
$clientes = $db->query("SELECT * FROM clientes WHERE activo = 1");
$totalClientes = count($clientes);
// ❌ Procesamiento de datos en view
foreach ($clientes as &$cliente) {
$cliente['nombre_completo'] = $cliente['nombre'] . ' ' . $cliente['apellido'];
}
?>
<h1>Total de clientes: <?= $totalClientes ?></h1>Solución: Separar lógica en controllers/services (arquitectura moderna).
4.2. Estado Global
Problema: JavaScript usa variables globales y estado compartido.
javascript
// ❌ Variables globales
let clienteActual = null;
let productoActual = null;
// ❌ Conflictos entre módulos
window.cargarCliente = function() { ... };Solución: Usar módulos ES6 con scope local (ya implementado en código moderno).
4.3. Manejo de Errores
Problema: Manejo inconsistente de errores.
javascript
// ❌ Sin manejo de errores
const response = await fetch(url);
const data = await response.json();
// ❌ Errores silenciosos
try {
await guardarCliente();
} catch (error) {
console.log(error); // Solo log, sin feedback al usuario
}Solución: Manejo consistente con feedback visual.
javascript
// ✅ Manejo completo
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
showSuccessMessage('Operación exitosa');
} catch (error) {
console.error('Error:', error);
showErrorMessage(error.message);
}4.4. Performance
Problemas identificados:
- Proxy deprecado: Doble salto HTTP innecesario
- No lazy loading: Modales cargados al inicio (no cuando se necesitan)
- jQuery excesivo: Manipulación DOM ineficiente
- Sin bundling: Múltiples requests HTTP para JS/CSS
Solución: Migración a React con Vite (bundling automático, code splitting).
5. Migración a React
Estrategia de Migración
ESTRATEGIA PROGRESIVA
La migración a React se hace gradualmente para minimizar riesgo y esfuerzo.
Paso 1: Identificar Candidatos
Priorizar módulos con:
- ✅ Alta complejidad de interactividad
- ✅ Múltiples estados y validaciones
- ✅ Reutilización en varios módulos
- ✅ Requisitos de performance
Paso 2: Elegir Tipo de Migración
| Tipo | Cuándo Usar | Referencia |
|---|---|---|
| Component Mount | Campo/modal pequeño embebido en view legacy | index.md - Component Mount |
| Full View React | View completa independiente | index.md - Full View React |
| Full Module | Módulo completo con routing | estructura-modulos-react.md |
Paso 3: Implementar React Component
Ejemplo: Migrar campo de cartera (Component Mount)
Legacy PHP + JS:
php
<!-- View legacy -->
<div class="form-group">
<label>Cartera</label>
<input id="inputCartera" type="text" class="form-control">
</div>
<script>
// JavaScript legacy
setAutocomplete(inputCartera, {
url: '/backend/carteras',
onSelect: (record) => {
cliente.cartera = record;
}
});
</script>Nuevo React Component:
typescript
// ts/ctacte/components/CarteraSelect.tsx
import React from 'react';
import { ControlledAutoComplete } from '../../core/components/form/ControlledAutoComplete';
interface CarteraSelectProps {
onChange: (record: Cartera) => void;
type: 'cliente' | 'proveedor';
className?: string;
}
export const CarteraSelect: React.FC<CarteraSelectProps> = ({
onChange,
type,
className
}) => {
return (
<div className={className}>
<label>Cartera</label>
<ControlledAutoComplete
url={`/carteras?tipo=${type}`}
displayField="nombre"
valueField="id"
placeholder="Ingrese y seleccione la cartera"
onSelect={onChange}
/>
</div>
);
};
export default CarteraSelect;Montaje en view legacy:
javascript
// js/view/mod-ventas/cliente.js (legacy)
import CarteraSelect from '../../../../dist/ctacte/components/CarteraSelect.js';
import { mountComponent } from '../../../../dist/main.js';
const props = {
onChange: (record) => {
Cliente.cartera = record;
},
type: 'cliente',
className: 'form-group col-md-6',
};
// Montar componente React en contenedor legacy
const api = await mountComponent(
form.querySelector('#containerCarteraField'),
CarteraSelect,
props
);Paso 4: Testing
typescript
// ts/ctacte/components/CarteraSelect.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CarteraSelect } from './CarteraSelect';
describe('CarteraSelect', () => {
it('llama onChange cuando se selecciona cartera', async () => {
const onChange = vi.fn();
render(
<CarteraSelect
onChange={onChange}
type="cliente"
/>
);
const input = screen.getByPlaceholderText(/ingrese y seleccione/i);
await userEvent.type(input, 'Cartera 1');
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ nombre: 'Cartera 1' })
);
});
});
});Paso 5: Deprecar Legacy
javascript
// ❌ Comentar o eliminar código legacy
/*
setAutocomplete(inputCartera, {
url: '/backend/carteras',
onSelect: (record) => {
cliente.cartera = record;
}
});
*/
// ✅ Usar componente React montado
const api = await mountComponent(...);Ejemplos de Migración Completa
Ejemplo 1: Conceptos de Retenciones (Full View React)
Legacy: bautista-app/view/mod-tesoreria/conceptos-retencion-ganancias.php (PHP + JS)
Nuevo: bautista-app/ts/tesoreria/views/ConceptosRetencionGanancias.tsx (React completo)
Diferencias:
| Aspecto | Legacy | React |
|---|---|---|
| Routing | PHP file-based | React Router (interno) |
| Estado | Variables globales | useState/useReducer |
| Validación | HTML5 + custom JS | Zod schemas |
| API | ApiRequest class | Axios con hooks |
| Testing | Manual | Vitest + Testing Library |
Ejemplo 2: Módulo Membresías (Full Module React)
Legacy: Múltiples archivos PHP + JS en mod-membresias/
Nuevo: bautista-app/ts/mod-membresias/ (React App completo)
Características:
- ✅ React Router con rutas propias
- ✅ Sidebar configurable
- ✅ Contextos propios (AuthContext, ConfigContext)
- ✅ Componentes reutilizables
- ✅ Testing completo
6. Troubleshooting
6.1. Modal no se carga
Síntomas: Modal no aparece o aparece vacío.
Causas comunes:
- Path incorrecto en
fetch() - HTML del modal malformado
- Bootstrap JS no inicializado
Solución:
javascript
// Verificar path completo
console.log('URL:', `${URL_APP}/php/components/mod-venta/modal.html`);
// Verificar respuesta
const response = await fetch(url);
console.log('Status:', response.status);
const html = await response.text();
console.log('HTML:', html);
// Verificar que Bootstrap esté cargado
if (typeof $.fn.modal === 'undefined') {
console.error('Bootstrap modal no está cargado');
}6.2. Autocomplete no funciona
Síntomas: Autocomplete no muestra sugerencias.
Causas comunes:
- URL del endpoint incorrecta
- Formato de respuesta del backend incorrecto
displayFieldovalueFieldno coinciden
Solución:
javascript
// Verificar configuración
setAutocomplete(input, {
url: `${URL_BACKEND}/clientes`,
displayField: 'nombre', // Debe coincidir con campo del backend
valueField: 'id',
minChars: 2,
onSelect: (record) => {
console.log('Record:', record); // Verificar estructura
}
});
// Verificar respuesta del backend en DevTools Network
// Debe ser: { data: [{ id: 1, nombre: 'Cliente 1' }, ...] }6.3. Error CORS en fetch
Síntomas: Access-Control-Allow-Origin error en consola.
Causas: Proxy deprecado o backend sin headers CORS.
Solución:
javascript
// ✅ Usar fetch directo al backend (no proxy)
const response = await fetch(`${URL_BACKEND}/clientes`);
// ⚠️ Si persiste, verificar backend CORS headers
// Backend debe incluir:
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Methods: GET, POST, PUT, DELETE
// Access-Control-Allow-Headers: Content-Type, X-Schema6.4. Multi-tenancy: Schema incorrecto
Síntomas: Datos de otra sucursal/caja aparecen en vista.
Causas: Header X-Schema no se envía o es incorrecto.
Solución:
javascript
// Verificar ApiRequest incluye X-Schema
const request = new ApiRequest();
console.log('Headers:', request.headers);
// Debe incluir: { 'X-Schema': 'suc0001' }
// Verificar localStorage
console.log('Schema:', localStorage.getItem('schema'));
// Verificar en DevTools Network que header se envía
// Headers > Request Headers > X-Schema: suc00016.5. DataTable no recarga
Síntomas: Tabla no muestra datos actualizados después de operación.
Causas: Cache de DataTables o evento no disparado.
Solución:
javascript
// Recargar tabla después de operación
try {
await request.post(`${URL_BACKEND}/clientes`, nuevoCliente);
// ✅ Recargar tabla
tablaClientes.ajax.reload(null, false); // null = callback, false = mantener página
showSuccessMessage('Cliente registrado');
} catch (error) {
console.error('Error:', error);
}
// Si persiste, destruir y recrear tabla
tablaClientes.destroy();
tablaClientes = $('#tablaClientes').DataTable({ ... });7. Referencias
Documentación Relacionada
- Arquitectura Frontend Moderna - React, TypeScript, Vite
- Estructura de Módulos React - Full Module pattern
- Arquitectura Backend - API Slim Framework
- Arquitectura Backend Legacy - Endpoints legacy
Ejemplos Reales en Código
| Módulo | Path Legacy | Path React Moderno |
|---|---|---|
| Ventas | js/view/mod-ventas/ | ts/ventas/ |
| Compras | js/view/mod-compras/ | (En migración) |
| Tesorería | js/view/mod-tesoreria/ | ts/tesoreria/ |
| Membresías | (Legacy eliminado) | ts/mod-membresias/ |
Tecnologías y Frameworks
- jQuery: https://jquery.com/
- Bootstrap 4: https://getbootstrap.com/docs/4.6/
- DataTables: https://datatables.net/
- Fetch API: https://developer.mozilla.org/es/docs/Web/API/Fetch_API
Migración a React
- React 19: https://react.dev/
- Vite: https://vitejs.dev/
- React Testing Library: https://testing-library.com/react
- Vitest: https://vitest.dev/
Resumen
RECORDATORIO FINAL
Esta arquitectura está DEPRECADA. Solo para mantenimiento de código existente.
Para nuevos desarrollos:
- ✅ Usar Arquitectura React Moderna
- ✅ Seguir patrones de Full Module
- ✅ Implementar testing con Vitest
Puntos clave:
- ✅ View PHP + JavaScript Vanilla legacy
- ❌ NO usar proxy
php/backend/(deprecado) - ✅ Fetch directo a backend API
- ✅ Modales cargados con
fetch()dinámico - ✅ ApiRequest class para comunicación HTTP
- ✅ Migración gradual a React siguiendo patrones modernos