Skip to content

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


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

Componentes 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)
    • PageHeader con titulo "Listado de articulos", boton "Nvo. Articulo | Alt+A", breadcrumbs [Home > Ventas > Articulos]
    • PageContent
      • ProductoTable con onEdit y showActivoColumn=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):

PropTipoDefaultDescripcion
reloadSignalnumber | undefined-Signal externo para forzar recarga de datos
enableSelectionbooleanfalseHabilitar checkboxes de seleccion
selectedProductoIdsnumber[]-IDs seleccionados (modo parcial)
onSelectionChange(producto, selected) => void-Callback al cambiar seleccion
selectionMode'partial' | 'all''partial'Modo de seleccion
excludedIdsnumber[]-IDs excluidos (modo 'all')
onSelectionModeChange(mode) => void-Callback al cambiar modo
showSelectionTogglebooleanfalseMostrar checkbox "seleccionar todos"
onEdit(producto) => void-Callback al presionar editar
showActivoColumnbooleantrueMostrar columna Estado

Columnas:

ColumnaaccessorKeyHeaderSizeFiltrableOrdenable
CodigoidCodigo100SiSi
NombrenombreNombre280SiSi
Codigo Comercialcodigo_comercialCodigo Comercial160SiSi
EstadoactivoEstado120NoNo

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):

ParametroTipoDefaultDescripcion
queryOptionsProductoQueryOptions{}Opciones de filtrado/paginacion
enabledbooleantrueHabilitar/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):

ParametroTipoDescripcion
reloadSignalnumber | undefinedSignal para forzar refetch

Retorno (compatibilidad con MaterialReactTable):

CampoTipoDescripcion
dataProductoListItem[]Datos de la pagina actual
meta{ totalRowCount } | nullMetadata de paginacion
isLoadingbooleanEstado de carga
errorError | nullError de la query
paginationMRT_PaginationStateEstado de paginacion
sortingMRT_SortingStateEstado de ordenamiento
columnFiltersMRT_ColumnFiltersStateEstado de filtros
onPaginationChangeDispatchHandler de cambio de paginacion
onSortingChangeDispatchHandler de cambio de ordenamiento
onColumnFiltersChangeDispatchHandler de cambio de filtros
queryResultobjectResultado 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:

ParametroTipoDescripcion
pageIndexnumberPagina actual (0-based)
pageSizenumberRegistros por pagina
orderOrder[]Ordenamiento [{ field, type }]
columnFilterColumnFilter[]Filtros por columna [{ field, search }]

Logica de respuesta adaptativa:

  1. Intenta con formato paginado moderno ({ data: [], meta: {} })
  2. Si detecta formato legacy ({ draw, recordsTotal, data }) lo transforma
  3. Si ninguno funciona, hace segunda peticion con parametros legacy SSR (serverSide, draw, start, length, search, order)

Mapeo de campos (FIELD_MAP):

FrontendBackend
idid
nombrenombre
codigo_comercialcodigo_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 HTMLNameTipo InputValidacion Frontend
Codigo comercialcodigoComercialtextmaxlength=14, blur verifica duplicidad
Nombrenombretextmaxlength=50, required
Descripciondescripciontextareamaxlength=400
Costocostotext (badge F)readonly en modificacion
% Gananciaporc_gananciatext (badge P)numerico
Agrupacionagrupaciontext (autocomplete)required
Linealineatext (autocomplete)required, disabled sin rubro
Proveedorproveedortext (autocomplete)opcional
Stockstockradio S/Ndefault S
Punto de pedidoptoPedidonumbermin=0
Imp internos tipotipoImpuestoradio P/Fdefault P
Imp internos valorimpuestotext (badge)numerico
ComisioncomisionArticulotext (badge P)0-100
Cat IVAselectCatIvaselectrequired
Maneja preciosmanejoPrecioscheckboxdefault checked
Ref contablereferenciatext (autocomplete)required (si visible)
ActivoidCheckActivocheckboxdisabled en alta, habilitado en mod
Ubicacionselect2 multi-nivelselect2 (5 niveles)tags habilitados
Membresia mesesmembresiaMeses[]select multiple (select2)12 opciones
Membresia aniomembresiaAnionumbermin=1900, max=9999
Membresia obsmembresiaObservaciontextareamaxlength=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 validaciones

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

CampoNameTipoValidacion
Listalistanumbermin=0, required, readonly en edicion
CostocostoListatext (badge F)Pre-cargado del articulo
% Utilporcentajetext (badge P)max 999.99%
Imp. Utilimportetext (badge F)readonly (calculado)
Preciopreciotext (badge F)required
Tipo preciotipoPrecioradio N/Fdefault N (Neto)

Calculo automatico: precio = costo + (costo * porcentaje / 100)

Autocompletes

CampoEndpointFormato LabelDatos guardados
Agrupacionrubro{id} | {concepto}Objeto completo
Linealinea (filtrado por rubro){id} | {descri}Objeto completo
Proveedorproveedor (scope min){id} | {nombre}Solo ID
Ref Contablereferencia-contable{codigo} | {nombre}Objeto completo

Ubicacion (Select2 jerarquico)

Implementacion: Sistema de 5 niveles jerarquicos con Select2.

Flujo:

  1. Se carga un select global con todas las ubicaciones disponibles
  2. Al seleccionar una ubicacion existente, se reconstruye la ruta jerarquica (niveles 1 a N)
  3. Al escribir un valor nuevo, se inicia desde nivel 1
  4. Cada cambio de nivel carga las opciones del nivel siguiente
  5. 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

VistaURLComponente/Archivo
Listado?loc=mvpproductos.php -> monta ProductoView (React)
Formulario?loc=mvfpform-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)

MetodoEndpointProposito
GETproductoListado paginado / SSR

Desde Vanilla JS (ApiRequest via proxy)

MetodoEndpoint ProxyProposito
GETproductoObtener producto por ID, listado con filtros
POSTproductoCrear producto
PUTproductoActualizar producto
GETrubroAutocomplete de rubros
GETlineaAutocomplete de lineas (filtrado por rubro)
GETproveedorAutocomplete de proveedores
GETreferencia-contableAutocomplete de refs contables
GETcategoria-ivaCarga de categorias IVA
GETempresDatos de empresa (config precios)
GETmod-ventas/ubicacionCarga 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.