Skip to content

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

ComponenteTecnologíaVersión
Server-SidePHP8.2+
Client-SideJavaScript VanillaES6+
DOM ManipulationjQuery3.x
CSS FrameworkBootstrap4.x
HTTP RequestsFetch APINativo

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

TipoPatternEjemplo
View PHP{nombre}.phptarjeta.php
Modal PHPmodal-{nombre}.phpmodal-item-subdiario.php
Modal HTMLmodal-{nombre}.htmlmodal-subdiario-venta.html
JavaScript View{nombre}.jstarjeta.js
JavaScript Modalform-{nombre}.js o list-{nombre}.jsform-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">&times;</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:

  1. ✅ View PHP renderiza estructura HTML
  2. ✅ JavaScript importa dependencias con ES6 modules
  3. ✅ Referencias a DOM con getElementById o jQuery
  4. ✅ Objeto de datos con métodos reset(), getData(), etc.
  5. ✅ Event listeners para interactividad
  6. ✅ 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}.js o módulo dedicado

Ejemplo completo:

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">&times;</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:

  1. ✅ Modal cargado con fetch() desde /php/components/
  2. ✅ HTML insertado dinámicamente en <body>
  3. ✅ JavaScript modular con IIFE o export
  4. ✅ Objeto de datos con getters/setters reactivos
  5. ✅ Autocomplete configurado con setAutocomplete()
  6. ✅ Validación con isValidForm()
  7. ✅ 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:

  1. Proxy deprecado: Doble salto HTTP innecesario
  2. No lazy loading: Modales cargados al inicio (no cuando se necesitan)
  3. jQuery excesivo: Manipulación DOM ineficiente
  4. 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

TipoCuándo UsarReferencia
Component MountCampo/modal pequeño embebido en view legacyindex.md - Component Mount
Full View ReactView completa independienteindex.md - Full View React
Full ModuleMódulo completo con routingestructura-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:

AspectoLegacyReact
RoutingPHP file-basedReact Router (interno)
EstadoVariables globalesuseState/useReducer
ValidaciónHTML5 + custom JSZod schemas
APIApiRequest classAxios con hooks
TestingManualVitest + 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:

  1. Path incorrecto en fetch()
  2. HTML del modal malformado
  3. 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:

  1. URL del endpoint incorrecta
  2. Formato de respuesta del backend incorrecto
  3. displayField o valueField no 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-Schema

6.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: suc0001

6.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

Ejemplos Reales en Código

MóduloPath LegacyPath React Moderno
Ventasjs/view/mod-ventas/ts/ventas/
Comprasjs/view/mod-compras/(En migración)
Tesoreríajs/view/mod-tesoreria/ts/tesoreria/
Membresías(Legacy eliminado)ts/mod-membresias/

Tecnologías y Frameworks

Migración a React


Resumen

RECORDATORIO FINAL

Esta arquitectura está DEPRECADA. Solo para mantenimiento de código existente.

Para nuevos desarrollos:

Puntos clave:

  1. ✅ View PHP + JavaScript Vanilla legacy
  2. ❌ NO usar proxy php/backend/ (deprecado)
  3. ✅ Fetch directo a backend API
  4. ✅ Modales cargados con fetch() dinámico
  5. ✅ ApiRequest class para comunicación HTTP
  6. ✅ Migración gradual a React siguiendo patrones modernos