Appearance
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.tsxVistas 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:
| Metodo | Endpoint | Proposito |
|---|---|---|
| GET | /mod-stock/tipo-comprobante | Listar todos |
| POST | /mod-stock/tipo-comprobante | Crear 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:
| Componente | Responsabilidad |
|---|---|
MovimientoForm | Formulario principal: fecha, comprobante (autocomplete), nrocomp, comentario, copias |
ArticulosTable | Tabla 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:
| Metodo | Endpoint | Proposito |
|---|---|---|
| GET | /mod-stock/tipo-comprobante | Autocomplete de tipo de comprobante |
| GET | /producto | Autocomplete de articulos |
| POST | /mod-stock/movimiento | Registrar 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:
ControlledAutoCompleteque cambia entre/rubroy/productosegun 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:
- Usuario completa filtros de rango (rubros y articulos) — defaults al montar via
useUltimoCostoDefaults - Busqueda carga tabla de articulos con costos actuales
- Usuario edita costos inline en
CostosTable(celdas editables) o abreCostoModalpor fila - Opcionalmente aplica porcentaje global via
PorcentajePanel(formula:costo * (1 + pct/100), 2 decimales) - Si hay filas modificadas y el usuario re-filtra, aparece dialog de confirmacion (
refilterDialogOpen) - Al confirmar el POST, se muestra dialog de confirmacion final
- Tras HTTP 201, redirige via
window.location.href
Componentes:
| Componente | Responsabilidad |
|---|---|
CostosTable | Tabla con edicion inline de costo. Resalta filas modificadas |
CostoModal | Modal por fila: articulo readonly, proveedor opcional, costo requerido |
PorcentajePanel | Input 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:
POSTal servicio de informes (puerto 9999)- Inyecta header
X-SchemadesdelocalStorage - 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)
| Archivo | Tests | Que valida |
|---|---|---|
ts/core/utils/generarInforme.test.ts | 6 | PDF, Excel, error 4xx/5xx, content-type desconocido, X-Schema header |
ts/stock/TipoComprobante/schemas/tipoComprobante.schema.test.ts | 6 | nombre requerido, max 50, trim |
ts/stock/TipoComprobante/services/tipoComprobante.service.test.ts | 4 | GET, POST, PUT |
ts/stock/TipoComprobante/hooks/useTipoComprobanteCrud.test.ts | 3 | Invalidacion cache en mutations |
ts/stock/Movimiento/schemas/movimiento.schema.test.ts | 10 | fecha, comprobante, nrocomp range, productos min(1) |
ts/stock/Movimiento/services/movimiento.service.test.ts | 2 | POST payload correcto |
ts/stock/Movimiento/hooks/useMovimientoSubmit.test.ts | 2 | generarInforme si imprimir='S', NO si imprimir!='S' |
ts/stock/UltimoCosto/schemas/ultimoCosto.schema.test.ts | 5 | rangos 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:
| Vista | codReporte |
|---|---|
| FichaStock | 'ficha_stock' (string) |
| ListadoExistencias | 'listado_existencias' (string) |
| PuntoPedido | 302 (number) |
| VariacionCosto | 301 (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.jsjs/view/mod-stock/ingreso-movimiento.jsjs/view/mod-stock/informes/ficha-stock.jsjs/view/mod-stock/informes/listado-existencias.jsjs/view/mod-stock/informes/punto-pedido.jsjs/view/mod-stock/informe-variacion-costo.jsjs/view/mod-stock/ultimo-costo.js
Referencias
- Tipo de Comprobante — Business Requirements
- Movimiento Stock Manual — Business Requirements
- Estructura de Modulos React
- generarInforme — Spec
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).