Skip to content

Patron de Migracion Sidebar TSX

Tipo: Patron de arquitectura frontend Ambito: Core + todos los modulos legacy (Contabilidad, Stock, CRM, Compras, Tesoreria, Ventas) Fecha: 2026-03-02 Estado: Implementado (6 modulos migrados)


Proposito

Este patron describe como migrar los sidebars PHP/JS legacy de cada modulo a un entry point React TSX, manteniendo compatibilidad total con la navegacion PHP (?loc=xxx), los modales legacy y el sistema de eventos del DOM.

Cuando aplicar este patron:

  • Al agregar un nuevo modulo que requiere sidebar reactivo
  • Al migrar un modulo legacy adicional al mismo patron

Cuando NO aplicar:

  • Modulos que ya son SPAs completas con HashRouter (CRM, Membresias) — esos usan {Module}App.tsx completo con Layout
  • Sidebars de paginas internas React — esas usan el componente Sidebar.tsx directamente dentro del app

Arquitectura: Piezas que intervienen

PHP Session / Logica de negocio
        |
        v
php/components/mod-{modulo}/main-sidebar-{modulo}.php
  - Prepara $sidebarConfig con permisos y flags
  - Emite: <div id="sidebar-{modulo}-app" data-configuration='...'></div>
  - Emite: <script type="module" src="dist/{modulo}/config/{Modulo}SidebarApp.js"></script>
        |
        v
ts/{modulo}/config/{Modulo}SidebarApp.tsx  (entry point React)
  - Lee data-configuration del DOM al DOMContentLoaded
  - Gestiona estado dinamico: certState, ejercicio, tipoCuenta, periodoText
  - Suscribe a custom events del DOM
  - Define customHandlers para los items modal-trigger
  - Llama mountApp(container, {Modulo}SidebarApp, { config })
        |
        v
ts/{modulo}/config/sidebar.ts  (configuracion de menu)
  - Exporta get{Modulo}SidebarMenuItems(config, ...dinamicParams): MenuItem[]
  - Usa createStandardSidebar() de ts/core/config/standardSidebarSections.ts
  - Aplica postProcess para convertir home.path -> home.href
  - Inyecta permissions en section wrappers
        |
        v
ts/core/components/layout/Sidebar.tsx  (componente core compartido)
  - Recibe: moduleTitle, moduleLoc, menuItems, userPermissions, customHandlers,
            customContentStart?, customContentEnd?
  - Filtra items por userPermissions
  - Renderiza href items como <a href="?loc=xxx"> (navegacion PHP full-page)
  - Renderiza modal-trigger items via customHandlers[item.id]
  - Detecta item activo via isHrefActive()

Flujo de navegacion en items legacy: todos los links a paginas PHP usan href: '?loc=xxx', nunca path. El MemoryRouter es obligatorio porque Sidebar.tsx llama useLocation() internamente, pero no se usa para ruteo real.


Estructura de archivos por modulo

Por cada modulo migrado se crean exactamente dos archivos nuevos:

bautista-app/ts/
└── {modulo}/
    └── config/
        ├── {Modulo}SidebarApp.tsx   -- entry point React (auto-detectado por Vite)
        └── sidebar.ts               -- configuracion de menu items

Y se modifica un archivo PHP existente:

bautista-app/php/components/mod-{modulo}/
└── main-sidebar-{modulo}.php        -- reemplaza el <aside> legacy por un <div> mount point

Auto-discovery de entry points: Vite detecta automaticamente todos los archivos *App.tsx dentro de ts/. No se necesita registrar el entry point manualmente en vite.config.ts. El output generado es dist/{modulo}/config/{Modulo}SidebarApp.js.


Componentes clave

createStandardSidebar(config)

Ubicacion: ts/core/config/standardSidebarSections.ts

Construye el array MenuItem[] con la estructura estandar de secciones:

  1. Home: "Menu modulo" (siempre presente)
  2. additionalItemsBefore[] — items extras antes de las secciones (ej: Seleccionar Ejercicio en Contabilidad)
  3. Bases
  4. Movimientos
  5. Informes
  6. Utilidades
  7. additionalItemsAfter[]
  8. CERRAR SESION (siempre al final)

Item home generado: usa path internamente. El sidebar.ts de cada modulo debe post-procesarlo a href via postProcess{Modulo}Sidebar(), porque la navegacion es PHP (?loc=xxx), no React Router.

Secciones vacias: si bases, movimientos, etc. son arrays vacios, esa seccion no se agrega.

postProcess{Modulo}Sidebar(items)

Funcion privada en cada sidebar.ts. Hace dos cosas:

  1. Convierte home.pathhome.href = '?loc={modulo}'
  2. Agrega permission a los section wrappers (bases, movimientos, informes, utilidades) para que Sidebar.tsx filtre la seccion completa cuando el usuario no tiene el permiso de nivel superior

Patron invariante en todos los modulos; solo cambian los prefijos de permisos (ej: VENTAS_BASES, CONTAB_BASES, etc.).

ModeChanger

Ubicacion: ts/core/components/ModeChanger.tsx

Componente self-contained que gestiona el modo de transacciones. Se inyecta via customContentEnd={<ModeChanger />} en el Sidebar.

Estado interno: lee y escribe localStorage.tipo_transaccion (valores: "0", "1", "2").

Ciclo de modos:

ValorClase CSSDescripcion
0bg-primaryPrueba (azul)
1bg-successOficial (verde)
2bg-consolidadosConsolidados

Contrato de eventos: despacha y escucha CustomEvent('modeChanged', { detail: number }) en document. Otros componentes de la pagina (modales legacy, handlers) escuchan este evento para adaptar su comportamiento.

Lock por URL: si window.location.search contiene ?prueba, el click muestra un modal informativo y NO cambia el modo ni escribe localStorage.

Modulos que incluyen ModeChanger: Contabilidad, Stock, CRM, Compras, Tesoreria, Ventas. Modulos que NO incluyen ModeChanger: Stock (el legacy sidebar de stock no tenia este boton).

Nota historica: ModeChanger fue creado inicialmente en ts/stock/config/ y luego movido a ts/core/components/ al detectarse que es compartido por 5 modulos.


Patron PHP mount point

Estructura del archivo PHP

php/components/mod-{modulo}/main-sidebar-{modulo}.php

Antes de la migracion: archivo con ~100-200 lineas de HTML PHP-gateado (<aside>...</aside>) + <script src="js/view/mod-{modulo}/sidebar.js">.

Despues de la migracion: archivo minimo con la preparacion del config y el div mount point.

Patron de codificacion JSON: siempre usar JSON_HEX_APOS | JSON_HEX_QUOT en el json_encode para evitar que comillas simples y dobles dentro del JSON rompan el atributo HTML.

Patron del contenedor: el <div> mount point NO necesita las clases CSS del <aside> original (main-sidebar sidebar-dark-primary elevation-4) porque Sidebar.tsx renderiza su propio <aside> con esas clases internamente. Sin embargo, si el layout de la pagina depende de que esas clases existan en el DOM antes de que React hidrate, pueden mantenerse en el <div> como fallback.

Script de carga: tipo module apuntando al JS compilado en dist/. El script legacy sidebar.js se elimina una vez que el smoke test del modulo pasa.

Esquema de configuracion por modulo

Los campos que PHP inyecta en data-configuration varian por modulo. Solo se inyectan valores estaticos disponibles en el momento del render PHP. Los valores dinamicos (periodo, ejercicio) se obtienen via API fetch o custom events del DOM despues del montaje.

Stock:

  • permisos: array de strings de permisos del usuario

CtaCte:

  • permisos: array de permisos
  • tipoCuenta: "D" (Deudor → label "Recibo") o "A" (Acreedor → label "Orden de Pago")

Tesoreria:

  • permisos: array de permisos
  • Periodo: NO se inyecta desde PHP; se obtiene via API al montar

Compras:

  • permisos: array de permisos
  • Periodo: NO se inyecta desde PHP; se obtiene via API al montar

Contabilidad:

  • permisos: array de permisos
  • ejercicioInicial: objeto { nro, desde, hasta } desde $_SESSION['EJERCICIO_SELECTED'] (puede ser null)
  • Periodo: NO se inyecta; se obtiene via API al montar

Ventas:

  • permisos: array de permisos
  • cartera: bool — flag de empresa para mostrar item "Carteras"
  • factManual: bool — flag de empresa para mostrar sub-menu "Registro Manual"
  • pedido: bool — flag de empresa para mostrar items de Pedidos
  • moduloContabilidad: bool — muestra "Ref. Contables" en Bases
  • moduloCtacte: bool — muestra "Clientes" y "Posicion de IVA" en Informes

Convenciones de iconos

Los iconos siguen una jerarquia visual consistente en todos los modulos:

NivelIconoUso
Seccion (colapsable)Icono semantico segun tipofas fa-toolbox (Bases), fas fa-truck-moving (Movimientos), fas fa-file-pdf (Informes), fas fa-wrench (Utilidades)
Item hijo (primer nivel)far fa-circleTodos los items directos dentro de una seccion
Item nieto (segundo nivel)far fa-dot-circleItems dentro de sub-secciones colapsables anidadas

Sub-secciones anidadas: cuando un item dentro de Bases, Movimientos, etc. tiene children, sus hijos usan far fa-dot-circle. Ejemplo: "Lista de precios" (circulo) → "Automatico por rango" (dot-circle), "Costo por margen" (dot-circle).


Items condicionales

Los items que solo aparecen segun flags de empresa o modulos habilitados se implementan con spread condicional en el array:

typescript
// Patron: spread condicional en el array de items
const basesItems: MenuItem[] = [
  // item siempre visible
  { id: 'cat-iva', ... },

  // item condicional: solo cuando config.cartera === true
  ...(config.cartera ? [{ id: 'cartera', ... } satisfies MenuItem] : []),

  // item condicional: solo cuando config.moduloContabilidad === true
  ...(config.moduloContabilidad ? [{ id: 'ref-contables', ... } satisfies MenuItem] : []),
];

Uso de satisfies MenuItem: garantiza que el objeto cumple el tipo MenuItem sin perder el tipo literal. Es el patron estandar para items dentro de spreads condicionales.

Flags que controlan items condicionales en Ventas:

  • config.cartera → "Carteras" en Bases
  • config.factManual → sub-menu "Registro Manual" en Movimientos
  • config.pedido → "Comp. pendientes" en Bases + sub-menu "Pedidos" en Movimientos
  • config.moduloContabilidad → "Ref. Contables" en Bases
  • config.moduloCtacte → "Clientes" en Informes + "Posicion de IVA" en Informes

Marcado de item activo

Sidebar.tsx detecta automaticamente el item activo comparando window.location.search con el href de cada item via isHrefActive().

Requisito critico: los scripts legacy de PHP que seteaban clases CSS de "activo" directamente sobre los elementos del sidebar HTML deben ser eliminados al migrar a TSX. Esos scripts buscaban elementos por ID (idMainSideBarClientes, etc.) que ya no existen en el DOM una vez que el sidebar es React. Si permanecen, lanzan errores de consola o intentan manipular elementos inexistentes.


Patron customHandlers

Los items que abren modales (sin navegacion) se declaran en sidebar.ts sin href, path ni onClick. El id del item es la clave de lookup en el mapa customHandlers que provee {Modulo}SidebarApp.tsx.

Declaracion en sidebar.ts:

typescript
// Item modal-trigger: sin href, sin path, sin onClick
{
  id: 'reimpresion-ventas',
  label: 'Reimpresion de Comprobante',
  icon: 'far fa-circle',
  permission: 'VENTAS_UTILS_REIMPRESION',
  // Sin href ni path — handler resuelto via customHandlers['reimpresion-ventas']
}

Definicion en {Modulo}SidebarApp.tsx:

typescript
const customHandlers: Record<string, () => void> = {
  'reimpresion-ventas': () => {
    // Llamada al modulo JS legacy o a funcion React
  },
};

Resolucion en Sidebar.tsx: const itemOnClick = item.onClick ?? customHandlers?.[item.id].

Dos tipos de handlers:

  1. Delegacion a modulo JS legacy: el handler llama directamente a la funcion importada del legacy JS. Se usa static import al inicio del archivo para que el modulo este pre-cargado (sin delay de importacion dinamica al hacer click). Los imports llevan @ts-ignore porque son modulos JS legacy sin tipos TypeScript.

  2. Delegacion a componente React: el handler llama a una funcion exportada por un modulo TSX del mismo dominio. Ejemplo en Ventas: 'comp-conceptos': () => showComprobantePorConceptosModal().


Comportamientos dinamicos por modulo

CtaCte: label dinamico Recibo / Orden de Pago

CtacteSidebarApp.tsx mantiene estado tipoCuenta inicializado desde config.tipoCuenta. Suscribe a CustomEvent('tipo-ctacte-change') en document para actualizarlo en tiempo real. El valor tipoCuenta se pasa como parametro a getCtacteSidebarMenuItems(config, tipoCuenta) y determina el label del item "Recibo" / "Orden de Pago" en Movimientos.

Tesoreria y Compras: periodo en curso

Ambos modulos usan el hook usePeriodoCurso(codigo) de ts/core/hooks/usePeriodoCurso.ts. El codigo es la constante del modulo (Periodo.TESORERIA, Periodo.COMPRAS). El hook hace fetch al endpoint de periodo y suscribe a CustomEvent('cambio-periodo-curso') filtrando por codigo. Retorna un string formateado "Periodo: Mar. de 2025" que se pasa como parametro a la funcion de menu items para mostrar en el sidebar como item no-interactivo.

Contabilidad: ejercicio y periodo

ContabilidadSidebarApp.tsx gestiona dos estados independientes: ejercicio (numero, desde, hasta) y periodoText (string). El ejercicio inicial puede venir de config.ejercicioInicial (sesion PHP). Al montar, si hay ejercicio inicial, lo propaga via evento ejercicioSeleccionado. Suscribe a:

  • ejercicioSeleccionado → actualiza estado React + DOM del navbar (span #ejercicio)
  • cambio-periodo-curso filtrado por codigo === CONTABILIDAD → actualiza span #periodo del navbar

El hook usePeriodoCurso(PERIODO_CONTABILIDAD) maneja el fetch y evento del periodo.

Contabilidad ademas hace fetch al endpoint de ejercicios abiertos al montar para determinar el comportamiento del handler 'seleccionar-ejercicio':

  • 0 ejercicios → abre modal de definicion
  • 1 ejercicio → muestra Swal info "Solo se posee un ejercicio contable"
  • >1 ejercicios → abre DataTable modal de seleccion

Ventas: alertas ARCA (certState)

VentasSidebarApp.tsx gestiona estado certState: string | null. Estrategia de inicializacion:

  1. Suscribe a window.addEventListener('certStateLoaded') — evento despachado por el script de verificacion de ARCA
  2. Fallback tras CERT_STATE_TIMEOUT (5000ms): lee localStorage.getItem('last_shown_cert_state')
  3. El useEffect retorna cleanup que remueve el listener y cancela el timeout

Los certAlerts son nodos JSX pasados via customContentStart al Sidebar. Se renderizan ANTES del primer item del menu.

Estados y sus efectos:

certStateAlerta visibleItem Facturas
'CERTIFICATE_EXPIRING_SOON'Warning amarillaHabilitado
'CERTIFICATE_EXPIRED'Danger rojaHabilitado
'CERTIFICATE_INVALID'Danger rojaHabilitado
'CREDENTIALS_NOT_FOUND'Danger rojaDeshabilitado (linkClassName: 'disabled pe-none text-muted')
nullNingunaHabilitado

Bug conocido: stale listener en modeChanged

Descripcion: en algunos handlers de {Modulo}SidebarApp.tsx que abren modales asincrono (ej: subdiario-ventas, reimpresion-ventas en Ventas), se necesita escuchar el evento modeChanged para actualizar el estado visual del modal (colores del header) cuando el usuario cambia de modo mientras el modal esta abierto.

El bug: si el listener modeChanged se agrega al modal al abrirlo y se olvida removerlo al cerrar, queda un listener "zombie" que referencia elementos del DOM ya eliminados. Esto produce errores de consola en el siguiente cambio de modo.

El patron correcto:

  1. Agregar el listener al abrir: document.addEventListener('modeChanged', onModeChanged)
  2. Removerlo en el evento de cierre del modal: $(modal).on('hidden.bs.modal', () => { document.removeEventListener('modeChanged', onModeChanged); contenedor.remove(); })

Este patron esta implementado correctamente en VentasSidebarApp.tsx en los handlers 'subdiario-ventas' y 'reimpresion-ventas'.


IDs de contenedores mount point

ModuloContainer ID
stocksidebar-stock-app
ctactesidebar-ctacte-app
tesoreriasidebar-tesoreria-app
comprassidebar-compras-app
contabilidadsidebar-contabilidad-app
ventassidebar-ventas-app

Como agregar un nuevo modulo

Pasos para aplicar el patron a un modulo nuevo (o no migrado):

1. Preparar el PHP mount point

Reemplazar el contenido de php/components/mod-{modulo}/main-sidebar-{modulo}.php:

  • Eliminar todo el bloque <aside>...</aside> existente
  • Preparar el array $sidebarConfig con los campos necesarios del modulo
  • Emitir el <div> mount point con data-configuration usando JSON_HEX_APOS | JSON_HEX_QUOT
  • Agregar el <script type="module"> apuntando a dist/{modulo}/config/{Modulo}SidebarApp.js
  • Verificar si hay templates HTML usados por modales legacy que deban mantenerse en el PHP (como <template id="template_reimpresion"> en Ventas)

Identificar y eliminar los scripts legacy que marcaban items como activos por ID de DOM (buscar referencias a idMainSideBar*).

2. Crear ts/{modulo}/config/sidebar.ts

  • Importar createStandardSidebar desde ts/core/config/standardSidebarSections.ts
  • Importar MenuItem desde ts/core/types/layout.types.ts
  • Definir la interfaz de configuracion {Modulo}SidebarConfig con los campos del data-configuration
  • Definir la funcion get{Modulo}SidebarMenuItems(config, ...params): MenuItem[]
  • Usar createStandardSidebar({ moduleHome: '?loc=xxx', bases: [...], movimientos: [...], informes: [...], utilidades: [...] })
  • Usar href: '?loc=xxx' en todos los items de navegacion PHP
  • Usar items sin href/path/onClick para los modal-triggers (el id es la clave del handler)
  • Agregar far fa-circle en items de primer nivel, far fa-dot-circle en nietos
  • Implementar postProcess{Modulo}Sidebar(items) para convertir home.pathhome.href y agregar permissions a las secciones

3. Crear ts/{modulo}/config/{Modulo}SidebarApp.tsx

  • Importar StrictMode, useState, useEffect de react
  • Importar MemoryRouter de react-router-dom
  • Importar Sidebar desde ts/core/components/layout/Sidebar.js
  • Importar ModeChanger desde ts/core/components/ModeChanger.js (si el modulo legacy tenia el boton M)
  • Importar get{Modulo}SidebarMenuItems y {Modulo}SidebarConfig desde ./sidebar.js
  • Importar mountApp desde ts/main.js
  • Importar con // @ts-ignore los modulos JS legacy necesarios para los handlers (static imports)
  • Definir la interfaz de props y la funcion componente
  • Gestionar estado dinamico del modulo con useState
  • Suscribir a custom events del DOM con useEffect (con cleanup en return)
  • Definir customHandlers: Record<string, () => void> con un entry por cada modal-trigger
  • Renderizar <StrictMode><MemoryRouter><Sidebar .../></MemoryRouter></StrictMode>
  • Incluir customContentEnd={<ModeChanger />} si el modulo lo requiere
  • Agregar el bloque de auto-mount con DOMContentLoaded al final del archivo

4. Verificar smoke test

Antes de eliminar el sidebar.js legacy del modulo, verificar:

  1. El sidebar renderiza visualmente (todas las secciones, labels correctos, logo)
  2. Los links href: '?loc=xxx' abren las paginas PHP correctas
  3. Los items permission-gateados solo aparecen para usuarios con permiso
  4. Los modal-triggers via customHandlers abren sus modales
  5. ModeChanger (si aplica) cicla correctamente: Prueba → Oficial → Consolidados → Prueba
  6. El evento modeChanged es recibido por los componentes legacy de la pagina
  7. localStorage.tipo_transaccion se actualiza en cada click
  8. El lock ?prueba=1 impide el cambio de modo y muestra modal informativo
  9. El comportamiento dinamico especifico del modulo funciona (certState, tipoCuenta, periodo, ejercicio)
  10. npx tsc --noEmit pasa sin errores
  11. npm run build completa sin errores

Diferencias con SPAs completas (CRM, Membresias)

AspectoSidebar TSX legacySPA completa (CRM/Membresias)
RouterMemoryRouterHashRouter
RutasNo usa rutasRutas React completas
Lo que renderizaSolo SidebarLayout completo
ConfigProviderNo requeridoRequerido
PermisosVia userPermissions prop directoVia ConfigContext
Navegacionhref a paginas PHPpath a rutas React

Archivos de referencia

  • ts/core/config/standardSidebarSections.ts — utilidad createStandardSidebar
  • ts/core/components/ModeChanger.tsx — componente compartido de modo
  • ts/core/components/layout/Sidebar.tsx — componente core de sidebar
  • ts/ventas/config/VentasSidebarApp.tsx — ejemplo mas complejo (certState + 12 handlers)
  • ts/ventas/config/sidebar.ts — ejemplo de menu con items condicionales y nested sections
  • ts/contabilidad/config/ContabilidadSidebarApp.tsx — ejemplo con ejercicio y periodo dinamico
  • ts/contabilidad/config/sidebar.ts — ejemplo con additionalItemsBefore
  • php/components/mod-venta/main-sidebar-venta.php — ejemplo de mount point PHP

Notas adicionales

  • El patron esta implementado para 6 modulos: Contabilidad, Stock, CRM, Compras, Tesoreria, Ventas
  • El orden de migracion historico fue: stock (piloto) → ctacte → tesoreria → compras → contabilidad → ventas
  • Stock es el mas simple: un solo custom handler, sin ModeChanger
  • Ventas es el mas complejo: 12 custom handlers, certState, items condicionales multiples
  • Los modulos Tesoreria y Compras comparten el mismo hook usePeriodoCurso parametrizado por codigo de modulo