Appearance
Articulos / Productos - Documentacion Tecnica Frontend
DOCUMENTACION RETROSPECTIVA - Generada a partir de codigo implementado el 2026-02-09
Modulo: mod-ventas Feature: Articulos / Productos Fecha: 2026-02-09
Link a Requisitos de Negocio
Articulos/Productos - Resource
Arquitectura del Frontend
Este recurso opera bajo una arquitectura hibrida:
- Listado: React/TypeScript moderno (ProductoView + ProductoTable)
- Formulario de alta/modificacion: PHP legacy + vanilla JavaScript
Listado (React moderno)
productos.php -> producto.js -> mountApp(ProductoView)
-> ProductoView.tsx
-> ProductoTable.tsx
-> useProductoTableSSR.ts
-> producto.service.ts
-> api.ts (axios con X-Schema)
Formulario (Legacy)
form-producto.php -> form-producto.js
-> ApiRequest (legacy)
-> public/php/backend/producto.php (proxy)
-> server/backend/producto.phpComponentes Implementados
ProductoView
Ubicacion: public/ts/mod-ventas/views/ProductoView.tsx
Proposito: Vista principal del listado de articulos. Monta la tabla de productos dentro de un layout estandar con encabezado, breadcrumbs y boton de nuevo articulo.
Estructura:
PageWrapper(layout)PageHeadercon titulo "Listado de articulos", boton "Nvo. Articulo | Alt+A", breadcrumbs [Home > Ventas > Articulos]PageContentProductoTablecononEdityshowActivoColumn=true
Navegacion:
- Boton "Nvo. Articulo": Redirige a
?loc=mvfp(formulario legacy) - Boton "Editar" (por fila): Redirige a
?loc=mvfp&id={productoId}(formulario legacy)
Nota: La navegacion entre el listado React y el formulario legacy se hace via window.location.href, no via React Router.
ProductoTable
Ubicacion: public/ts/mod-ventas/components/ProductoTable.tsx
Proposito: Tabla de productos reutilizable con paginacion, ordenamiento y filtrado server-side. Autonoma (obtiene datos internamente). Usada en multiples modulos (ventas, membresias).
Props (ProductoTableProps):
| Prop | Tipo | Default | Descripcion |
|---|---|---|---|
| reloadSignal | number | undefined | - | Signal externo para forzar recarga de datos |
| enableSelection | boolean | false | Habilitar checkboxes de seleccion |
| selectedProductoIds | number[] | - | IDs seleccionados (modo parcial) |
| onSelectionChange | (producto, selected) => void | - | Callback al cambiar seleccion |
| selectionMode | 'partial' | 'all' | 'partial' | Modo de seleccion |
| excludedIds | number[] | - | IDs excluidos (modo 'all') |
| onSelectionModeChange | (mode) => void | - | Callback al cambiar modo |
| showSelectionToggle | boolean | false | Mostrar checkbox "seleccionar todos" |
| onEdit | (producto) => void | - | Callback al presionar editar |
| showActivoColumn | boolean | true | Mostrar columna Estado |
Columnas:
| Columna | accessorKey | Header | Size | Filtrable | Ordenable |
|---|---|---|---|---|---|
| Codigo | id | Codigo | 100 | Si | Si |
| Nombre | nombre | Nombre | 280 | Si | Si |
| Codigo Comercial | codigo_comercial | Codigo Comercial | 160 | Si | Si |
| Estado | activo | Estado | 120 | No | No |
Columna de seleccion (condicional): Se antepone si enableSelection=true. Incluye checkbox por fila y opcional checkbox "seleccionar todos" en header.
Renderizador de Estado: Muestra "ACTIVO" o "INACTIVO" segun valor booleano.
Acciones: Boton de edicion por fila (tipo 'edit') si se proporciona callback onEdit.
Componente base: Usa DataTable del core (core/components/DataTable).
Hooks Personalizados
useProductoList
Ubicacion: public/ts/mod-ventas/hooks/useProducto.ts
Proposito: Hook basico para obtener lista de productos con paginacion SSR. Wrapper sobre useQuery de TanStack Query.
Parametros (UseProductoListOptions):
| Parametro | Tipo | Default | Descripcion |
|---|---|---|---|
| queryOptions | ProductoQueryOptions | {} | Opciones de filtrado/paginacion |
| enabled | boolean | true | Habilitar/deshabilitar la query |
Configuracion de cache: Perfil WARM (getProfileConfig('WARM'))
Query Key: ventasQueryKeys.productos.list(queryOptions) -> tenant-aware key
useProductoTableSSR
Ubicacion: public/ts/mod-ventas/hooks/useProductoTableSSR.ts
Proposito: Hook completo para gestionar el estado de la tabla de productos con server-side rendering. Encapsula paginacion, ordenamiento, filtrado y sincronizacion con backend.
Parametros (UseProductoTableSSROptions):
| Parametro | Tipo | Descripcion |
|---|---|---|
| reloadSignal | number | undefined | Signal para forzar refetch |
Retorno (compatibilidad con MaterialReactTable):
| Campo | Tipo | Descripcion |
|---|---|---|
| data | ProductoListItem[] | Datos de la pagina actual |
| meta | { totalRowCount } | null | Metadata de paginacion |
| isLoading | boolean | Estado de carga |
| error | Error | null | Error de la query |
| pagination | MRT_PaginationState | Estado de paginacion |
| sorting | MRT_SortingState | Estado de ordenamiento |
| columnFilters | MRT_ColumnFiltersState | Estado de filtros |
| onPaginationChange | Dispatch | Handler de cambio de paginacion |
| onSortingChange | Dispatch | Handler de cambio de ordenamiento |
| onColumnFiltersChange | Dispatch | Handler de cambio de filtros |
| queryResult | object | Resultado completo de la query |
Hook base: Usa useServerSideTable del core (core/hooks/useServerSideTable)
Query Keys: Usa membresiaQueryKeys.productos (nota: no ventasQueryKeys como en useProductoList - posible inconsistencia).
Estado inicial: pageIndex: 0, pageSize: 10, sorting: [{ id: 'id', desc: false }]
Types
ProductoListItem
Ubicacion: public/ts/mod-ventas/types/producto.types.ts
typescript
export interface ProductoListItem extends Record<string, unknown> {
id: number;
nombre: string;
codigo_comercial?: string | null;
activo?: boolean | null;
}Servicio API
ProductoService
Ubicacion: public/ts/mod-ventas/services/producto.service.ts
Endpoint: producto (relativo, apunta a backend/producto.php via proxy Axios)
Metodos:
getAll(options: ProductoQueryOptions)
Proposito: Obtener lista de productos paginada. Soporta tanto respuesta paginada moderna como formato legacy DataTable SSR.
Parametros de query:
| Parametro | Tipo | Descripcion |
|---|---|---|
| pageIndex | number | Pagina actual (0-based) |
| pageSize | number | Registros por pagina |
| order | Order[] | Ordenamiento [{ field, type }] |
| columnFilter | ColumnFilter[] | Filtros por columna [{ field, search }] |
Logica de respuesta adaptativa:
- Intenta con formato paginado moderno (
{ data: [], meta: {} }) - Si detecta formato legacy (
{ draw, recordsTotal, data }) lo transforma - Si ninguno funciona, hace segunda peticion con parametros legacy SSR (
serverSide, draw, start, length, search, order)
Mapeo de campos (FIELD_MAP):
| Frontend | Backend |
|---|---|
| id | id |
| nombre | nombre |
| codigo_comercial | codigo_comercial |
State Management
Server State (TanStack Query)
Query Keys (ventasQueryKeys):
typescript
ventasQueryKeys.productos.all() // ['ventas', 'productos']
ventasQueryKeys.productos.lists() // ['ventas', 'productos', 'list']
ventasQueryKeys.productos.list(opts) // ['ventas', 'productos', 'list', opts]Nota: El hook useProductoTableSSR usa membresiaQueryKeys.productos en vez de ventasQueryKeys.productos. Esto podria ser intencional (para invalidacion cruzada con membresias) o un error.
Cache Profile: WARM (staleTime y gcTime moderados)
Local State
No hay estado local complejo en los componentes React. El estado de la tabla (paginacion, filtros, ordenamiento) se gestiona internamente por useServerSideTable.
Formulario Legacy (form-producto.js)
Estructura del objeto articulo
El formulario vanilla JS gestiona un objeto articulo con la siguiente estructura:
javascript
{
id: null, // Numerico o null (alta)
codigo_comercial: null, // String o null
nombre: null, // String
descripcion: null, // String o null
costo: null, // Numerico
rubro: null, // Objeto { id, concepto }
linea: null, // Objeto { id, descri }
ref_con: null, // Objeto { codigo, nombre }
maneja_stock: 'S', // 'S' o 'N'
punto_pedido: null, // Numerico
imp_interno: null, // Numerico
tipo_imp: 'P', // 'P' o 'F'
categoria_iva: null, // Objeto { codigo, nombre }
manejo_precios_facturacion: null, // Boolean
comision: null, // Numerico (0-100)
activo: true, // Boolean
porc_ganancia: null, // Numerico
proveedor: null, // Numerico (ID)
ubicacion: [], // Array de objetos jerarquicos
listas: [], // Array de { lista, precio, tipo_precio }
membresia: null // Objeto { meses, anio, observacion } o null
}Campos del formulario
| Campo HTML | Name | Tipo Input | Validacion Frontend |
|---|---|---|---|
| Codigo comercial | codigoComercial | text | maxlength=14, blur verifica duplicidad |
| Nombre | nombre | text | maxlength=50, required |
| Descripcion | descripcion | textarea | maxlength=400 |
| Costo | costo | text (badge F) | readonly en modificacion |
| % Ganancia | porc_ganancia | text (badge P) | numerico |
| Agrupacion | agrupacion | text (autocomplete) | required |
| Linea | linea | text (autocomplete) | required, disabled sin rubro |
| Proveedor | proveedor | text (autocomplete) | opcional |
| Stock | stock | radio S/N | default S |
| Punto de pedido | ptoPedido | number | min=0 |
| Imp internos tipo | tipoImpuesto | radio P/F | default P |
| Imp internos valor | impuesto | text (badge) | numerico |
| Comision | comisionArticulo | text (badge P) | 0-100 |
| Cat IVA | selectCatIva | select | required |
| Maneja precios | manejoPrecios | checkbox | default checked |
| Ref contable | referencia | text (autocomplete) | required (si visible) |
| Activo | idCheckActivo | checkbox | disabled en alta, habilitado en mod |
| Ubicacion | select2 multi-nivel | select2 (5 niveles) | tags habilitados |
| Membresia meses | membresiaMeses[] | select multiple (select2) | 12 opciones |
| Membresia anio | membresiaAnio | number | min=1900, max=9999 |
| Membresia obs | membresiaObservacion | textarea | maxlength=500 |
Campos Condicionales por Módulos
El formulario implementa lógica de visibilidad condicional basada en los módulos habilitados:
JavaScript (form-producto.js líneas ~45-80):
javascript
if (!permisos.modulo_contabilidad) {
$('#containerRefContable').remove();
}
if (!permisos.modulo_compras) {
$('#containerPuntoReposicion, #containerProveedor').remove();
}
// ... más validacionesComportamiento Backend: La falta de datos por módulos no se valida en backend sino que suelen tener campos por defecto como deshabilitación (Null, 0, N, false, etc...). Los campos eliminados del DOM no se envían y el backend los trata como NULL/0/false según corresponda.
Campo Costo - Readonly en Edición
El campo "Costo" tiene atributo readonly por defecto y solo se remueve cuando es un producto nuevo:
javascript
if (!updateData) {
inputCostoArticulo.removeAttribute('readonly');
}Razón de negocio: El costo se actualiza desde el cambio en lista de precios o desde compras si se posee el módulo. Esto previene modificaciones manuales que podrían romper la trazabilidad del costo.
Extensión de Membresía
Sección colapsable que permite definir:
- Meses disponibles: String de 12 caracteres (ej: "111000000000" = enero-marzo)
- Año: Año de vigencia
- Observación: Notas adicionales
Propósito: Se puede designar en qué momento se facturan algunos productos. Útil para productos de facturación estacional o periódica en el módulo de membresías.
Modal de lista de precios
| Campo | Name | Tipo | Validacion |
|---|---|---|---|
| Lista | lista | number | min=0, required, readonly en edicion |
| Costo | costoLista | text (badge F) | Pre-cargado del articulo |
| % Util | porcentaje | text (badge P) | max 999.99% |
| Imp. Util | importe | text (badge F) | readonly (calculado) |
| Precio | precio | text (badge F) | required |
| Tipo precio | tipoPrecio | radio N/F | default N (Neto) |
Calculo automatico: precio = costo + (costo * porcentaje / 100)
Autocompletes
| Campo | Endpoint | Formato Label | Datos guardados |
|---|---|---|---|
| Agrupacion | rubro | {id} | {concepto} | Objeto completo |
| Linea | linea (filtrado por rubro) | {id} | {descri} | Objeto completo |
| Proveedor | proveedor (scope min) | {id} | {nombre} | Solo ID |
| Ref Contable | referencia-contable | {codigo} | {nombre} | Objeto completo |
Ubicacion (Select2 jerarquico)
Implementacion: Sistema de 5 niveles jerarquicos con Select2.
Flujo:
- Se carga un select global con todas las ubicaciones disponibles
- Al seleccionar una ubicacion existente, se reconstruye la ruta jerarquica (niveles 1 a N)
- Al escribir un valor nuevo, se inicia desde nivel 1
- Cada cambio de nivel carga las opciones del nivel siguiente
- Soporte para
tags: true(crear nuevas ubicaciones sobre la marcha)
Formato de datos guardados:
javascript
[
{ id: 1, nombre: "Deposito A", nivel: 1, parent_id: null },
{ id: 3, nombre: "Estante 1", nivel: 2, parent_id: 1 }
]Membresia (conversion meses)
Formato de almacenamiento: String de 12 caracteres (ej: "111111000000" = enero a junio).
- Posicion 0 = Enero, Posicion 11 = Diciembre
- '1' = disponible, '0' = no disponible
Funciones de conversion:
mesesStringToArray("111111000000")->[1, 2, 3, 4, 5, 6]mesesArrayToString([1, 2, 3])->"111000000000"
Routing
| Vista | URL | Componente/Archivo |
|---|---|---|
| Listado | ?loc=mvp | productos.php -> monta ProductoView (React) |
| Formulario | ?loc=mvfp | form-producto.php -> form-producto.js (vanilla JS) |
| Formulario (edicion) | ?loc=mvfp&id={id} | Mismo formulario en modo edicion |
Navegacion sidebar: Seccion "Bases" > "Productos" (activado via JS en la pagina)
Integracion con Backend
Endpoints Consumidos
Desde React (ProductoService via api.ts)
| Metodo | Endpoint | Proposito |
|---|---|---|
| GET | producto | Listado paginado / SSR |
Desde Vanilla JS (ApiRequest via proxy)
| Metodo | Endpoint Proxy | Proposito |
|---|---|---|
| GET | producto | Obtener producto por ID, listado con filtros |
| POST | producto | Crear producto |
| PUT | producto | Actualizar producto |
| GET | rubro | Autocomplete de rubros |
| GET | linea | Autocomplete de lineas (filtrado por rubro) |
| GET | proveedor | Autocomplete de proveedores |
| GET | referencia-contable | Autocomplete de refs contables |
| GET | categoria-iva | Carga de categorias IVA |
| GET | empres | Datos de empresa (config precios) |
| GET | mod-ventas/ubicacion | Carga de ubicaciones |
Testing
No se encontraron tests unitarios ni de integracion para los componentes React de Producto en el directorio tests/.
Decisiones Tecnicas Observadas
Arquitectura hibrida
El listado se migrO a React (ProductoView + ProductoTable) pero el formulario sigue siendo PHP + vanilla JS. La navegacion entre ambos es via redirect (window.location.href).
Soporte dual de respuestas
El ProductoService soporta tanto respuesta paginada moderna como formato legacy DataTable SSR, permitiendo compatibilidad durante la transicion.
Componente de tabla reutilizable
ProductoTable fue disenado como componente autonomo (obtiene datos internamente) y reutilizable (usado tanto en Ventas como en Membresias con diferentes props de seleccion).
Query keys inconsistentes
El hook useProductoList usa ventasQueryKeys.productos mientras que useProductoTableSSR usa membresiaQueryKeys.productos. Esto puede causar problemas de invalidacion de cache.
Referencias
NOTA IMPORTANTE: Esta documentacion fue generada automaticamente analizando el codigo implementado. Validar cambios futuros contra este baseline.