Saltar a contenido

Componente: CreacionProductos

Autor: Adalberto González

Selector: app-creacion-productos

Ubicación: SitioLogiGho/src/app/components/creacion-productos


¿Qué hace?

Modal de creación masiva de productos. Permite al usuario rellenar uno o varios formularios en paralelo, seleccionar tienda, categoría, proveedor y tipo de producto mediante buscadores con dropdown, subir una imagen a S3 y guardar cada producto en la base de datos con un ID, SKU e ID de variación generados de forma consecutiva.


Roles y acceso

Acceso Descripción
Autenticado Requiere sesión activa. La apertura del modal la controla el componente padre (PortafolioComponent), que a su vez está protegido por AuthGuard.

Estructura de archivos

creacion-productos/
├── creacion-productos.component.ts
├── creacion-productos.component.html
└── creacion-productos.component.scss

Inputs y Outputs

Decorador Nombre Tipo Descripción
@Input isOpen boolean Controla si el modal es visible. El padre lo pone en true al abrir y en false al cerrar.
@Input transactionId string ID de transacción inyectado por el padre (actualmente recibido pero no utilizado internamente).
@Output closeEvent EventEmitter<void> Emitido al llamar a close(). El padre lo escucha para ocultar el modal.
@Output refreshDataEvent EventEmitter<void> Emitido al terminar una creación exitosa. El padre lo escucha en onProductoCreado() para recargar la tabla.

Interfaz Producto (interna)

interface Producto {
  _id?: { $oid: string } | string;
  id: number | null;
  nombre: string;
  imagenUrl: string;
  precioventa: number | null;
  cantidad: number | null;
  idproducto: number | null;
  idvariacion: string;
  variacion: string;
  precioproveedor: number | null;
  sku: string;
  tipoproducto: string;
  IdTienda?: string;
  Tienda?: string;
  proveedor: string;
  categoria: string;
  peso: number | null;
  largo: number | null;
  ancho: number | null;
  alto: number | null;
  color: string;
  estado: string;
  perfilproducto: string;
  FechaCreacionProducto?: string;
}

Propiedades del componente

Formulario

Propiedad Tipo Default Descripción
formGroupWrapper FormGroup Formulario raíz que envuelve el FormArray.
formArray FormArray [] Array de FormGroup, uno por cada producto a crear en lote.
nuevoProducto Producto objeto vacío Objeto auxiliar donde se acumula la tienda, proveedor, categoría e imagen antes de construir productoParaGuardar.

Listas de datos

Propiedad Tipo Descripción
tiendas any[] Tiendas activas cargadas desde Tienda en la BD.
categorias any[] Categorías cargadas desde ProductosCategorias.
proveedores any[] Subconjunto de tiendas cuyo TipoTienda incluye "Proveedor".
tipoProductoOpciones { Nombre, Value }[] Lista estática: Dropshipping, Propia, Proveedor.

Estado general

Propiedad Tipo Default Descripción
isLoading boolean false Bloquea el botón "Guardar" mientras se procesa la creación.
imagenesSeleccionadas (File \| null)[] [] Imagen seleccionada por formulario (indexada por posición en formArray).
consecutivoVariacion number 1 Contador que se incrementa al generar cada idvariacion dentro de un lote.
rows TablaRow[] [] Datos de todos los productos cargados (usado solo para calcular IDs consecutivos).
rowsMemory any[] [] Buffer intermedio del procesamiento por bloques de fetchTableData().

Estado de dropdowns (por formulario)

Cada uno de los siguientes es un array indexado por posición en formArray:

Propiedad Tipo Descripción
buscarCategorias String[] Texto escrito en el buscador de categoría.
mostrarCategoriasMenu boolean[] Visibilidad del dropdown de categoría.
categoriasFiltradas any[][] Lista filtrada de categorías para mostrar.
categoriaSeleccionadaNombre string[] Nombre de la categoría seleccionada (mostrado en el placeholder).
buscarProveedores String[] Texto escrito en el buscador de proveedor.
mostrarProveedoresMenu boolean[] Visibilidad del dropdown de proveedor.
proveedoresFiltrados any[][] Lista filtrada de proveedores.
proveedorSeleccionadoNombre String[] Nombre del proveedor seleccionado.
buscarTiendas String[] Texto escrito en el buscador de tienda.
mostrarTiendasMenu boolean[] Visibilidad del dropdown de tienda.
tiendasFiltradas any[][] Lista filtrada de tiendas.
tiendaSeleccionadaNombre String[] Nombre de la tienda seleccionada.
buscarTipoProducto String[] Texto escrito en el buscador de tipo.
mostrarTipoProductoMenu boolean[] Visibilidad del dropdown de tipo.
tipoProductoFiltradas any[][] Lista filtrada de tipos de producto.
tipoProductoSeleccionadoNombre String[] Nombre del tipo seleccionado.

Secciones de la vista (modal)

# Sección Descripción
1 Cabecera Título "Creación de Productos" y botón de cierre (×).
2 Formulario de producto (*ngFor sobre formArray) Para cada producto: campos de características, imagen, dimensiones, variación/color, etiquetas (tipo, proveedor, tienda) y otros (estado, perfil).
3 Buscadores con dropdown Categoría, tipo de producto, proveedor y tienda usan un input de búsqueda con dropdown custom en lugar de <select>.
4 Botón "Añadir Producto" Llama a addFormularioProducto() para agregar otro bloque de formulario.
5 Footer Botones "Cerrar" (close()) y "Guardar" (onCreateOrModify()). El botón "Guardar" se deshabilita mientras isLoading es true.

Flujo de inicialización

ngOnInit()
  → addFormularioProducto()       (agrega el primer FormGroup)
  → fetchTableDataTienda()        (carga tiendas activas desde BD)
  → fetchTableDataCategoria()     (carga categorías desde BD)
  → fetchTableDataProveedor()     (carga proveedores desde BD)
  → tiendaService.tiendas$.subscribe()  (suscripción reactiva, sobreescribe this.tiendas)

ngOnChanges()
  → si isOpen cambia a true:
      → fetchTableData()          (carga todos los productos para cálculo de IDs)

Métodos

addFormularioProducto()

Agrega un nuevo bloque de formulario al lote.

Proceso: 1. Obtiene el índice actual (formArray.length) 2. Inicializa las posiciones [index] de todos los arrays de estado de dropdown en sus valores vacíos/falsos 3. Llama a createProductoForm() y hace push al formArray


removeFormularioProducto(index)

Elimina el formulario en la posición index del lote.

Proceso: 1. formArray.removeAt(index) 2. Hace splice(index, 1) en todos los arrays de estado de dropdown para mantener sincronía con formArray


createProductoForm(): FormGroup

Retorna un FormGroup nuevo con todos los controles del formulario de un producto (nombre, imagenUrl, precioventa, cantidad, precioproveedor, sku, tipoproducto, Tienda, IdTienda, proveedor, categoria, nombreTienda, nombreProveedor, nombreCategoria, peso, largo, ancho, alto, color, estado, perfilproducto).


onCreateOrModify(): Promise<void>

Método principal de guardado. Crea todos los productos del lote secuencialmente.

Proceso: 1. Bloquea re-envíos con isLoading 2. Muestra Swal.fire de carga con spinner 3. Llama a fetchTableData() para obtener IDs actualizados (mitiga race condition) 4. Calcula idProductoGenerado y idGenerado base con obtenerIdProductoConsecutivo() / obtenerIdConsecutivo() 5. Itera sobre cada FormGroup del formArray: - Genera el SKU base: NOM-variacion-COL - Verifica unicidad del SKU con verificarExistenciaSKU(); si existe, agrega consecutivo (-1, -2, …) - Valida que tienda, categoría y proveedor estén seleccionados - Valida que haya una imagen seleccionada - Sube la imagen con cargarImagenProducto() - Resuelve nombres de tienda, proveedor y categoría buscando en sus listas locales - Construye productoParaGuardar con todos los campos - Llama a insertarGenerico(productoParaGuardar, 'productos') - Llama a insertarGenerico(bodyActualizaInventario, 'actualizaInventarioLotes') para actualizar stock - Incrementa idProductoGenerado 6. Emite refreshDataEvent y llama a close() al finalizar el lote 7. En finally: libera isLoading

Manejo de error: Cualquier excepción muestra Swal.fire de error.


fetchTableData(): Promise<void> (privado)

Carga la totalidad de productos de la BD para calcular IDs consecutivos correctos.

Proceso: 1. Consulta página 1 para obtener NumeroPaginas 2. Crea grupos de 8 páginas y las procesa en paralelo con Promise.all 3. Descomprime cada página con decompressGzip 4. Procesa los datos en bloques de 128 items con processBlockAsync() (usa requestIdleCallback si disponible) 5. Genera columnas con generateColumns() y asigna this.rows

Nota: Este método es costoso (carga todos los productos). Se llama al abrir el modal (ngOnChanges) y al inicio de onCreateOrModify() para reducir la ventana de race condition en la generación de IDs.


fetchTableDataTienda(): Promise<void>

Carga las tiendas activas desde la colección Tienda (filtradas por Estado=ACTIVO). Descomprime con decompressZstd. Ordena alfabéticamente y mapea a { Nombre, Id }.


fetchTableDataCategoria(): Promise<void>

Carga categorías desde ProductosCategorias. Descomprime con decompressGzip. Mapea a { Nombre, Id }.


fetchTableDataProveedor(): Promise<void>

Carga tiendas con TipoTienda que contiene "Proveedor" desde la colección Tienda. Descomprime con decompressZstd. Mapea a { Nombre, Id, TipoTienda }.


verificarExistenciaSKU(sku): Promise<boolean>

Consulta metodoGenerico?coleccion=Productos&sku={sku}. Retorna true si hay al menos un resultado, false si no. Usado dentro del loop de onCreateOrModify() para garantizar unicidad de SKU.


onFileSelected(event, index)

Valida que el archivo sea image/jpeg, image/png o image/webp. Si el formato no es válido muestra Swal de error y limpia el input. Si es válido, guarda el File en imagenesSeleccionadas[index].


imagenPrevia(file): string

Retorna URL.createObjectURL(file) para la previsualización en el template. Retorna '' si file es null.


cargarImagenProducto(key, userData): Promise<void>

Sube una imagen al bucket S3 imagenes-productos-logigho.

Proceso: 1. Convierte el archivo a base64 con convertirABase64() 2. Extrae la parte base64 pura (sin prefijo MIME) 3. Llama a putObjectService.cargarInformacion() con el base64, bucket, clave y extensión 4. Asigna la URL CloudFront a nuevoProducto.imagenUrl


obtenerIdConsecutivo(): number

Retorna Math.max(...rows.map(p => p.id)) + 1. Retorna 1 si no hay productos.


obtenerIdProductoConsecutivo(): number

Retorna Math.max(...rows.map(p => p.idproducto)) + 1. Retorna 10001 si no hay productos.


obtenerIdConsecutivoVariacion(): string

Retorna el valor actual de consecutivoVariacion como string y lo incrementa. Se llama una vez por producto dentro del lote para generar IDs de variación únicos dentro de esa sesión.


generarFechaCreacion(): string

Retorna la fecha y hora actual ajustada a UTC-5 en formato "YYYY-MM-DD HH:mm:ss".


Métodos de dropdown (patrón repetido para categoría, proveedor, tienda y tipo de producto)

Método Descripción
filtrarXxx(formIndex) Filtra la lista completa según el texto en buscarXxx[formIndex] (case-insensitive) y actualiza xxxFiltrados[formIndex].
xxxSeleccionado(item, formIndex) Guarda el nombre del ítem en xxxSeleccionadoNombre[formIndex], limpia el campo de búsqueda, cierra el menú y hace patchValue en el FormControl correspondiente del FormGroup (guarda el ID del ítem, no el nombre).
abrirXxxMenu(formIndex) Cierra todos los menús, abre el de xxx en formIndex y puebla la lista sin filtro.
alternadorXxxMenu(formIndex) Alterna la visibilidad del menú de xxx en formIndex. Si se abre, puebla la lista sin filtro.
cerrarTodosLosMenus() Escuchador @HostListener('document:click'). Pone todos los mostrarXxxMenu en false.

Servicios Angular utilizados

Servicio Métodos usados Propósito
ConsumoGenericoService consultarGenerico(), insertarGenerico() Consulta de productos, tiendas, categorías y proveedores; inserción de productos e inventario
DecompressionService decompressGzip(), decompressZstd() Descomprime las respuestas del backend
PutObjectService cargarInformacion() Sube imágenes a S3
GetObjectService Inyectado pero sin uso activo en este componente
TiendasService tiendas$ Suscripción reactiva al listado de tiendas (sobreescribe el resultado de fetchTableDataTienda)
FormBuilder group(), array() Construcción de formularios reactivos

Dependencias externas

Librería Uso
sweetalert2 Alertas de carga, éxito y error durante la creación
rxjs/firstValueFrom Convierte observables en promesas dentro de los métodos async
xlsx Importado pero sin uso activo en este componente

Endpoints que consume

Método Ruta Cuándo
GET metodoGenerico?coleccion=Productos Al abrir el modal y al iniciar onCreateOrModify() (todas las páginas)
GET metodoGenerico?coleccion=Tienda&Estado=ACTIVO Al inicializar (tiendas y proveedores)
GET metodoGenerico?coleccion=ProductosCategorias Al inicializar (categorías)
GET metodoGenerico?coleccion=Productos&sku={sku} Por cada producto, para verificar unicidad de SKU
POST productos (vía insertarGenerico) Por cada producto aprobado en el lote
POST actualizaInventarioLotes (vía insertarGenerico) Inmediatamente tras insertar cada producto

Changelog del componente

Fecha Autor Cambio
2026-04-20 Por definir Migración de <select> nativos a buscadores con dropdown custom para categoría, proveedor, tienda y tipo de producto
2026-04-20 Por definir Separación del estado de dropdown en arrays indexados para soportar creación en lote (múltiples formularios)
2026-04-20 Por definir Adición de fetchTableData() al inicio de onCreateOrModify() para reducir ventana de race condition en generación de IDs
2026-04-20 Por definir .trim() sobre tiendaSeleccionada.Nombre al asignar nuevoProducto.Tienda para evitar espacios que rompan el filtro de portafolio
2026-04-20 Por definir Adición de llamada a actualizaInventarioLotes tras cada inserción para registrar ingreso de inventario

Observaciones

  • Race condition en IDs: Se generan en frontend, riesgo de duplicados. Debe hacerlo el backend.
  • consecutivoVariacion no se reinicia: Sigue aumentando si no se destruye el componente.
  • Conflicto en this.tiendas: Dos formatos distintos causan errores.
  • loadFile() incompleto: No descarga nada.
  • transactionId sin uso: No se utiliza.
  • Validación débil: Sin Validators, permite envíos inválidos.