Skip to content

Stock — Arquitectura Frontend React (Migracion Legacy)

DOCUMENTACION RETROSPECTIVA - Generada a partir de codigo implementado el 2026-03-22

Modulo: Stock Feature: Migracion completa de vistas legacy (PHP/jQuery) a React Fecha: 2026-03-22


Contexto

El modulo Stock tenia 7 vistas de negocio implementadas con PHP SSR + jQuery/Vanilla JS. Este cambio (stock-frontend-migration) reemplaza todas ellas por componentes React, siguiendo el patron establecido en otros modulos del sistema (ctacte, compras, ventas).

El backend ya habia sido modernizado en stock-refactor con endpoints Slim Framework. Esta migracion completa el ciclo: el frontend ahora consume exclusivamente los contratos Slim modernos (id/nombre, PUT /{id}) sin dependencias del legacy PHP.


Arquitectura Implementada

Patron: Componentes React montables (mount containers) — sin React Router, sin SPA completa.

Cada vista es una aplicacion React independiente montada en un <div id="...-app"> dentro de su .php correspondiente. El sidebar del modulo (StockSidebarApp.tsx) ya existia; estas vistas se integran al mismo patron sin modificarlo.

Entry Point Pattern

Todos los *App.tsx del modulo siguen el patron identico:

typescript
document.addEventListener('DOMContentLoaded', () => {
    const container = document.getElementById('{entidad}-app');
    if (!container) return;
    const configuration = JSON.parse(container.dataset.configuration ?? '{}');
    mountApp(
        createElement(ModalRegistryProvider, null,
            createElement(ConfigProvider, { config: configuration },
                createElement({Entidad}View, null)
            )
        ),
        container
    );
});

Detalle: ModalRegistryProvider envuelve a ConfigProvider — este orden es obligatorio para que los modales del sistema (confirmacion, alertas) funcionen correctamente.


Estructura de Directorios

ts/stock/
├── config/
│   ├── StockSidebarApp.tsx      # PRE-EXISTENTE — sin cambios
│   ├── sidebar.ts               # PRE-EXISTENTE — sin cambios
│   └── queryKeys.ts             # NUEVO — query keys para todo el modulo stock

├── TipoComprobante/             # CRUD estandar (GenericCrudView)
│   ├── components/TipoComprobanteForm/index.tsx
│   ├── hooks/
│   │   ├── useTipoComprobante.ts
│   │   └── useTipoComprobanteCrud.ts
│   ├── schemas/tipoComprobante.schema.ts
│   ├── services/tipoComprobante.service.ts
│   ├── types/tipoComprobante.types.ts
│   ├── views/TipoComprobanteView.tsx
│   └── TipoComprobanteApp.tsx

├── Movimiento/                  # Formulario complejo con tabla inline
│   ├── components/
│   │   ├── ArticulosTable/index.tsx
│   │   └── MovimientoForm/index.tsx
│   ├── hooks/
│   │   ├── useTipoComprobanteParaMovimiento.ts
│   │   └── useMovimientoSubmit.ts
│   ├── schemas/movimiento.schema.ts
│   ├── services/movimiento.service.ts
│   ├── types/movimiento.types.ts
│   ├── views/IngresoMovimientoView.tsx
│   └── IngresoMovimientoApp.tsx

├── FichaStock/                  # Informe: filtros + trigger PDF
│   ├── schemas/, types/, views/
│   └── FichaStockApp.tsx

├── ListadoExistencias/          # Informe: filtros complejos (radios, checkbox, autocomplete)
│   ├── hooks/useListadoExistenciaDefaults.ts
│   ├── schemas/, types/, views/
│   └── ListadoExistenciasApp.tsx

├── PuntoPedido/                 # Informe: rango de rubros
│   ├── hooks/useRubros.ts
│   ├── schemas/, types/, views/
│   └── PuntoPedidoApp.tsx

├── VariacionCosto/              # Informe: articulo requerido
│   ├── schemas/, types/, views/
│   └── VariacionCostoApp.tsx

└── UltimoCosto/                 # Formulario avanzado: tabla editable + modal individual
    ├── components/
    │   ├── CostosTable/
    │   ├── CostoModal/
    │   └── PorcentajePanel/
    ├── hooks/
    │   ├── useUltimoCostoDefaults.ts
    │   └── useUltimoCostoSubmit.ts
    ├── schemas/, types/, views/
    └── UltimoCostoApp.tsx

Vistas Implementadas

1. TipoComprobante — CRUD Estandar

Patron: GenericCrudView<TipoComprobante>PHP view: view/mod-stock/bases-comprobantes.phpEntry point: dist/stock/TipoComprobante/TipoComprobanteApp.jsModulo referencia: ctacte/Cartera

Funcionalidad:

  • Listado de tipos de comprobante con columna nombre
  • Crear nuevo tipo via modal (campo nombre, max 50 chars)
  • Editar tipo existente via modal
  • Eliminar con confirmacion

Contrato API:

MetodoEndpointProposito
GET/mod-stock/tipo-comprobanteListar todos
POST/mod-stock/tipo-comprobanteCrear nuevo
PUT/mod-stock/tipo-comprobante/{id}Actualizar

Tipos:

typescript
interface TipoComprobante { id: number; nombre: string; }
type TipoComprobanteFormData = { nombre: string };

Validacion Zod: z.string().min(1).max(50).trim()


2. IngresoMovimiento — Formulario Complejo

Patron: PageWrapper + PageHeader + tabla de articulos inline PHP view: view/mod-stock/ingreso-movimiento.phpEntry point: dist/stock/Movimiento/IngresoMovimientoApp.jsModulo referencia: mod-ventas/registro-manual

Guard de modulo: La vista verifica config.modulos.modulo_controlstock === 1 al montar. Si el modulo no esta habilitado, muestra un Alert de error y no renderiza el formulario.

Componentes principales:

ComponenteResponsabilidad
MovimientoFormFormulario principal: fecha, comprobante (autocomplete), nrocomp, comentario, copias
ArticulosTableTabla inline con add/edit/delete de articulos. Estado local useState<ProductoItem[]>

Estado de articulos: useState<ProductoItem[]> local en MovimientoForm. No usa TanStack Query (es estado de sesion del formulario, no server-state).

Flujo de impresion: Tras registro exitoso, si comprobante.imprimir === 'S' llama a generarInforme({ codReporte: 'ingreso_movimiento', ...payload }).

Contrato API:

MetodoEndpointProposito
GET/mod-stock/tipo-comprobanteAutocomplete de tipo de comprobante
GET/productoAutocomplete de articulos
POST/mod-stock/movimientoRegistrar movimiento

Tipos clave:

typescript
interface ProductoItem {
    unique: string;  // identificador local de fila (no enviado al backend)
    id: number;
    nombre: string;
    costo: number;
    cantidad: number;
}
interface MovimientoPayload {
    fecha: string;          // ISO YYYY-MM-DD
    comprobante: number;    // id del tipo comprobante
    nrocomp: number | null;
    comentario: string | null;
    copias: number;
    productos: Omit<ProductoItem, 'unique'>[];
    marca: 'O';             // siempre 'O' (Oficial) en movimientos manuales
}

3. FichaStock — Informe

Patron: Formulario filtros + trigger generarInformePHP view: view/mod-stock/ficha-stock.phpEntry point: dist/stock/FichaStock/FichaStockApp.js

Filtros:

  • Articulo (autocomplete, opcional)
  • Fecha desde / hasta (default: hoy)

Payload informe:

typescript
generarInforme({
    codReporte: 'ficha_stock',
    cod: producto.id | null,
    descri: producto.nombre | null,
    fechaDesdeHasta: `${fechaDesde}/${fechaHasta}`,
    tipo: 1
})

Nota de implementacion: El campo descri (nombre del producto) puede llegar como null si el usuario no selecciona un producto. El backend acepta null en ambos campos. Ver verify-report.md para detalle del gap en onValueChange de ControlledAutoComplete.


4. ListadoExistencias — Informe

Patron: Formulario filtros complejos + trigger generarInformePHP view: view/mod-stock/listado-existencias.phpEntry point: dist/stock/ListadoExistencias/ListadoExistenciasApp.js

Filtros:

  • Tipo de listado: radio (listado/informe/orden)
  • Agrupacion dinamica: ControlledAutoComplete que cambia entre /rubro y /producto segun tipo
  • Checkbox de opcion adicional
  • Fecha desde / hasta (con validacion fechaDesde <= fechaHasta)

Logica condicional: El campo de informe (tipo) se deshabilita cuando listado === 2 (modo productos). El endpoint del autocomplete de agrupacion cambia segun el tipo de listado seleccionado.

Defaults: useListadoExistenciaDefaults carga el primer y ultimo rubro via GET /rubro.


5. PuntoPedido — Informe

Patron: Formulario filtros + trigger generarInformePHP view: view/mod-stock/informe-punto-pedido.phpEntry point: dist/stock/PuntoPedido/PuntoPedidoApp.js

Filtros:

  • Rango de agrupacion: desde / hasta (autocomplete /rubro, defaults primer/ultimo)

Payload informe: { codReporte: 302, opcion: 1, agrupacionDesde, agrupacionHasta }

Validacion de rango: Zod schema verifica agrupacionDesde.id <= agrupacionHasta.id.


6. VariacionCosto — Informe

Patron: Formulario filtros + trigger generarInformePHP view: view/mod-stock/informe-variacion-costo.phpEntry point: dist/stock/VariacionCosto/VariacionCostoApp.js

Filtros:

  • Articulo (autocomplete, requerido)

Payload informe: { codReporte: 301, articulo: articulo.id }


7. UltimoCosto — Formulario Avanzado

Patron: PageWrapper + tabla editable inline + modal individual + panel porcentaje PHP view: view/mod-stock/ultimo-costo.phpEntry point: dist/stock/UltimoCosto/UltimoCostoApp.js

Flujo de uso:

  1. Usuario completa filtros de rango (rubros y articulos) — defaults al montar via useUltimoCostoDefaults
  2. Busqueda carga tabla de articulos con costos actuales
  3. Usuario edita costos inline en CostosTable (celdas editables) o abre CostoModal por fila
  4. Opcionalmente aplica porcentaje global via PorcentajePanel (formula: costo * (1 + pct/100), 2 decimales)
  5. Si hay filas modificadas y el usuario re-filtra, aparece dialog de confirmacion (refilterDialogOpen)
  6. Al confirmar el POST, se muestra dialog de confirmacion final
  7. Tras HTTP 201, redirige via window.location.href

Componentes:

ComponenteResponsabilidad
CostosTableTabla con edicion inline de costo. Resalta filas modificadas
CostoModalModal por fila: articulo readonly, proveedor opcional, costo requerido
PorcentajePanelInput de porcentaje + boton "Aplicar a todos". Formula: costo * (1 + pct/100)

Defaults al montar (useUltimoCostoDefaults — 4 queries en paralelo):

  • Primer rubro (GET /rubro?first=true)
  • Ultimo rubro (GET /rubro?last=true)
  • Primer articulo (GET /producto?first=true)
  • Ultimo articulo (GET /producto?last=true)

Contrato API (POST):

typescript
// POST /mod-stock/movimiento
{
    nrocomp: 0,
    comprobante: { codigo: 0, tipo: 'I' },
    productos: ProductoModificado[],
    marca: 'O'
}

Utilidad Compartida: generarInforme

Ubicacion: ts/core/utils/generarInforme.tsTipo: Helper TypeScript (no React, no hook)

Creada en esta migracion como reemplazo TypeScript del legacy js/middleware/informes.js. Se ubica en ts/core/utils/ porque es transversal a modulos (usada por Stock, y disponible para cualquier modulo que necesite generar informes).

Interfaz:

typescript
interface InformePayload {
    codReporte: string | number;
    [key: string]: unknown;
}

async function generarInforme(payload: InformePayload): Promise<void>

Comportamiento:

  • POST al servicio de informes (puerto 9999)
  • Inyecta header X-Schema desde localStorage
  • Si Content-Type: application/pdf → abre PDF en nueva pestana (window.open)
  • Si Content-Type: application/vnd.openxmlformats... → descarga Excel
  • Si HTTP 4xx → lanza error con body.error
  • Si HTTP 5xx → lanza error con mensaje especifico de estado
  • Content-type desconocido → lanza error

Query Keys del Modulo

Ubicacion: ts/stock/config/queryKeys.tsExport: stockQueryKeys

typescript
export const stockQueryKeys = {
    all: () => createTenantQueryKey('stock'),
    tipoComprobante: {
        all: () => createTenantQueryKey('stock', 'tipo-comprobante'),
        list: () => createTenantQueryKey('stock', 'tipo-comprobante', 'list'),
    },
};

Las queries de useRubros y otras compartidas usan createTenantQueryKey directamente sin pasar por stockQueryKeys (patron inline, aceptable para queries de entidades globales como rubros y productos).


PHP Views: Mount Container Pattern

Todas las vistas PHP siguen el mismo patron:

php
<!-- Mount container con configuracion serializada -->
<div id="{entidad}-app" data-configuration="<?= htmlspecialchars(json_encode($configuration)) ?>"></div>

<!-- Script React (modulo ES) -->
<script type="module" src="<?= $baseUrl ?>dist/stock/{Entidad}/{Entidad}App.js"></script>

<!-- Legacy comentado (pendiente eliminacion tras validacion en staging) -->
<!--
<script src="<?= $baseUrl ?>js/view/mod-stock/{legacy-file}.js"></script>
-->

Estado de limpieza: Los 7 archivos JS legacy permanecen en el repositorio comentados en los .php hasta confirmar todas las vistas en staging. La eliminacion definitiva se hace cuando todas las vistas esten validadas en produccion.


Testing

38 tests — 0 fallos (Vitest)

ArchivoTestsQue valida
ts/core/utils/generarInforme.test.ts6PDF, Excel, error 4xx/5xx, content-type desconocido, X-Schema header
ts/stock/TipoComprobante/schemas/tipoComprobante.schema.test.ts6nombre requerido, max 50, trim
ts/stock/TipoComprobante/services/tipoComprobante.service.test.ts4GET, POST, PUT
ts/stock/TipoComprobante/hooks/useTipoComprobanteCrud.test.ts3Invalidacion cache en mutations
ts/stock/Movimiento/schemas/movimiento.schema.test.ts10fecha, comprobante, nrocomp range, productos min(1)
ts/stock/Movimiento/services/movimiento.service.test.ts2POST payload correcto
ts/stock/Movimiento/hooks/useMovimientoSubmit.test.ts2generarInforme si imprimir='S', NO si imprimir!='S'
ts/stock/UltimoCosto/schemas/ultimoCosto.schema.test.ts5rangos rubros y articulos

Estrategia: Unit tests para services (vi.mock de api.ts), hooks (renderHook + QueryClientWrapper), y schemas (Zod puro). Sin tests de integracion UI (React Testing Library) en esta migracion.


Integracion con el Sistema de Informes

Las 4 vistas de informes (FichaStock, ListadoExistencias, PuntoPedido, VariacionCosto) consumen el servicio externo de generacion de documentos en puerto 9999 via generarInforme.

Codigos de reporte:

VistacodReporte
FichaStock'ficha_stock' (string)
ListadoExistencias'listado_existencias' (string)
PuntoPedido302 (number)
VariacionCosto301 (number)

Convivencia Legacy / React

Durante la transicion, cada vista .php mantiene el script legacy en un bloque comentado. El rollback de cualquier vista es inmediato: descomentar el script legacy y remover el mount container.

Los archivos JS legacy no se eliminan del repositorio hasta validacion completa en staging:

  • js/view/mod-stock/comprobantes.js
  • js/view/mod-stock/ingreso-movimiento.js
  • js/view/mod-stock/informes/ficha-stock.js
  • js/view/mod-stock/informes/listado-existencias.js
  • js/view/mod-stock/informes/punto-pedido.js
  • js/view/mod-stock/informe-variacion-costo.js
  • js/view/mod-stock/ultimo-costo.js

Referencias


NOTA: Esta documentacion fue generada como post-apply gate del cambio stock-frontend-migration. Refleja el codigo implementado y verificado (verify-report: PASS WITH WARNINGS, 38/38 tests pass).