diff --git a/src/Dominio/Modelos/Productos/InfoProducto.js b/src/Dominio/Modelos/Productos/InfoProducto.js index d9ef51e9..6dde4a9d 100644 --- a/src/Dominio/Modelos/Productos/InfoProducto.js +++ b/src/Dominio/Modelos/Productos/InfoProducto.js @@ -21,6 +21,7 @@ export class InfoProducto { this.envio = producto.envio ?? 0; this.impuesto = producto.impuesto ?? 0; this.descuento = producto.descuento ?? 0; + this.descripcion = producto.descripcion ?? ''; // Variantes del producto (puede ser un arreglo vacío) this.variantes = Array.isArray(producto.variantes) @@ -42,14 +43,14 @@ class VarianteProducto { class OpcionVariante { constructor({ - estado, - cantidad, - descuento, - valorOpcion, - SKUcomercial, - SKUautomatico, - costoAdicional, - }) { + estado, + cantidad, + descuento, + valorOpcion, + SKUcomercial, + SKUautomatico, + costoAdicional, + }) { this.estado = estado ?? null; this.cantidad = cantidad ?? 0; this.descuento = descuento ?? 0; diff --git a/src/Dominio/Repositorios/Productos/RepositorioActualizarProducto.js b/src/Dominio/Repositorios/Productos/RepositorioActualizarProducto.js new file mode 100644 index 00000000..e0a5f273 --- /dev/null +++ b/src/Dominio/Repositorios/Productos/RepositorioActualizarProducto.js @@ -0,0 +1,118 @@ +// RF [29] Actualiza Producto - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF29] +import axios from 'axios'; +import { RUTAS_API } from '@Utilidades/Constantes/rutasAPI'; +import ProductoCompleto from '@Modelos/Productos/ProductoCompleto'; +import Variante from '@Modelos/Productos/Variante'; +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioActualizarProducto { + /** + * Actualiza un producto y sus variantes en el backend. + * @param {Object} productoRaw - Datos del producto a actualizar. + * @param {Array} variantesRaw - Lista de variantes del producto. + * @param {File|null} imagenProducto - Imagen del producto. + * @param {Object} imagenesVariantes - Mapa de imágenes por variante. + * @returns {Promise} Respuesta del servidor. + */ static async actualizarProducto({ + productoRaw, + variantesRaw, + imagenProducto, + imagenesVariantes, + }) { + try { + const form = new FormData(); + + // Verificar que tengamos un ID de producto válido + if (!productoRaw.idProducto) { + console.error('Error: No se encontró el ID del producto en los datos recibidos'); + throw new Error( + 'El ID del producto es requerido para actualizar. Por favor, asegúrate de seleccionar un producto válido.' + ); + } + + // Asegurarnos de que el ID del producto sea un string + form.append('idProducto', String(productoRaw.idProducto)); // Limpiar el objeto producto antes de enviarlo + const productoLimpio = { + idProducto: productoRaw.idProducto, // Asegurarnos de incluir el idProducto + nombreComun: productoRaw.nombreComun, + nombreComercial: productoRaw.nombreComercial, + descripcion: productoRaw.descripcion, + marca: productoRaw.marca, + modelo: productoRaw.modelo, + tipoProducto: productoRaw.tipoProducto, + precioPuntos: Number(productoRaw.precioPuntos), + precioCliente: Number(productoRaw.precioCliente), + precioVenta: Number(productoRaw.precioVenta), + costo: Number(productoRaw.costo), + impuesto: Number(productoRaw.impuesto), + descuento: Number(productoRaw.descuento), + estado: Number(productoRaw.estado), + envio: Number(productoRaw.envio), + idProveedor: productoRaw.idProveedor, // Agregar también el idProveedor + }; + + // Agregar el producto como JSON string + form.append('producto', JSON.stringify(productoLimpio)); + + // Limpiar y formatear las variantes + const variantesLimpias = variantesRaw.map((variante) => ({ + identificador: String(variante.identificador), + nombreVariante: variante.nombreVariante, + descripcion: variante.descripcion, + opciones: Array.isArray(variante.opciones) + ? variante.opciones.map((opcion) => ({ + valorOpcion: opcion.valorOpcion, + cantidad: Number(opcion.cantidad), + SKUautomatico: opcion.SKUautomatico, + SKUcomercial: opcion.SKUcomercial, + costoAdicional: Number(opcion.costoAdicional || 0), + descuento: Number(opcion.descuento || 0), + estado: Number(opcion.estado || 1), + })) + : [], + })); + + // Agregar las variantes como JSON string + form.append('variantes', JSON.stringify(variantesLimpias)); + + // Agregar imagen del producto si existe + if (imagenProducto instanceof File) { + form.append('imagenProducto', imagenProducto); + } // Agregar imágenes de variantes y construir el mapa + const mapaImagenes = []; + if (imagenesVariantes && typeof imagenesVariantes === 'object') { + for (const [idVariante, imagenesArray] of Object.entries(imagenesVariantes)) { + if (Array.isArray(imagenesArray)) { + for (const img of imagenesArray) { + if (img && img.file instanceof File) { + form.append('imagenesVariante', img.file); + mapaImagenes.push({ + filename: img.file.name, + idVariante: String(idVariante), + }); + } + } + } + } + } + + // Agregar el mapa de imágenes como JSON string + form.append('mapaImagenes', JSON.stringify(mapaImagenes)); // Log para depuración + + const respuesta = await axios.post(RUTAS_API.PRODUCTOS.ACTUALIZAR_PRODUCTO, form, { + withCredentials: true, + headers: { + 'x-api-key': API_KEY, + 'Content-Type': 'multipart/form-data', + Accept: 'application/json', + }, + transformRequest: [(data) => data], // Prevenir que axios transforme el FormData + }); + + return respuesta.data; + } catch (error) { + const mensaje = error?.response?.data?.mensaje || 'Error al actualizar el producto'; + throw new Error(mensaje); + } + } +} diff --git a/src/Utilidades/Constantes/rutasAPI.js b/src/Utilidades/Constantes/rutasAPI.js index 9e0caec1..30d5fbb5 100644 --- a/src/Utilidades/Constantes/rutasAPI.js +++ b/src/Utilidades/Constantes/rutasAPI.js @@ -44,7 +44,8 @@ export const RUTAS_API = { ELIMINAR_PRODUCTO: `${BASE_PRODUCTOS}/eliminar`, IMPORTAR: `${BASE_PRODUCTOS}/importar`, LEER_PRODCUTO: `${BASE_PRODUCTOS}/leer-producto`, - EXPORTAR_PRODUCTOS: `${BASE_PRODUCTOS}/exportar-productos` + EXPORTAR_PRODUCTOS: `${BASE_PRODUCTOS}/exportar-productos`, + ACTUALIZAR_PRODUCTO: `${BASE_PRODUCTOS}/actualizar`, }, PROVEEDORES: { BASE: BASE_PROVEEDORES, diff --git a/src/Utilidades/Validaciones/validarProducto.js b/src/Utilidades/Validaciones/validarProducto.js index 3ba73b57..3c21e294 100644 --- a/src/Utilidades/Validaciones/validarProducto.js +++ b/src/Utilidades/Validaciones/validarProducto.js @@ -51,28 +51,35 @@ export const validarProducto = (producto) => { } else if (!/^[1-9]\d{0,7}(\.\d{1,2})?$/.test(producto.costo.toString())) { errores.costo = 'El costo debe tener máximo 10 dígitos.'; } - // Validación de impuesto if (producto.impuesto === false) { errores.impuesto = 'El impuesto no es válido o el campo está vacío.'; - } - if (typeof producto.impuesto === 'number') { - if (!/^(0|[1-9]\d{0,4})(\.\d{1,2})?$/.test(producto.impuesto.toString())) { - errores.impuesto = 'El impuesto debe ser un número válido con máximo 5 dígitos.'; + } else if (producto.impuesto !== undefined && producto.impuesto !== null) { + if (typeof producto.impuesto !== 'number' && isNaN(Number(producto.impuesto))) { + errores.impuesto = 'El impuesto debe ser un número válido.'; + } else if (Number(producto.impuesto) < 0) { + errores.impuesto = 'El impuesto no puede ser negativo.'; + } else if (Number(producto.impuesto) > 99999.99) { + errores.impuesto = 'El impuesto no puede ser mayor a 99999.99.'; + } else if (!/^(0|[1-9]\d{0,4})(\.\d{1,2})?$/.test(producto.impuesto.toString())) { + errores.impuesto + = 'El impuesto debe ser un número válido con máximo 5 dígitos enteros y 2 decimales.'; } } - // Validación de descuento - - if (producto.descuento > 100) { - errores.descuento = 'El descuento debe estar entre 0 y 100.'; - } + // Validación de descuento if (producto.descuento === false) { errores.descuento = 'El descuento no es válido o el campo está vacío.'; - } - if (typeof producto.descuento === 'number') { - if (!/^(0|[1-9]\d{0,4})(\.\d{1,2})?$/.test(producto.descuento.toString())) { - errores.descuento = 'El descuento debe ser un número válido con máximo 5 dígitos.'; + } else if (producto.descuento !== undefined && producto.descuento !== null) { + if (typeof producto.descuento !== 'number' && isNaN(Number(producto.descuento))) { + errores.descuento = 'El descuento debe ser un número válido.'; + } else if (Number(producto.descuento) < 0) { + errores.descuento = 'El descuento no puede ser negativo.'; + } else if (Number(producto.descuento) > 100) { + errores.descuento = 'El descuento debe estar entre 0 y 100.'; + } else if (!/^(0|[1-9]?\d|100)(\.\d{1,2})?$/.test(producto.descuento.toString())) { + errores.descuento + = 'El descuento debe ser un número válido entre 0 y 100 con hasta 2 decimales.'; } } diff --git a/src/Utilidades/Validaciones/validarVariantes.js b/src/Utilidades/Validaciones/validarVariantes.js index 5e048413..9331e7a7 100644 --- a/src/Utilidades/Validaciones/validarVariantes.js +++ b/src/Utilidades/Validaciones/validarVariantes.js @@ -41,13 +41,17 @@ export const validarVariantes = (variantes) => { erroresOpcion.valorOpcion = 'El valor de la opción es obligatorio'; } else if (opcion.valorOpcion.trim().length > 50) { erroresOpcion.valorOpcion = 'El valor de la opción debe tener máximo 50 caracteres'; - } - - // Validación de cantidad - if (!Number.isFinite(opcion.cantidad) || opcion.cantidad <= 0) { - erroresOpcion.cantidad = 'La cantidad debe ser un número mayor a 0'; - } else if (!/^\d{1,10}$/.test(opcion.cantidad.toString())) { - erroresOpcion.cantidad = 'La cantidad debe tener máximo 10 dígitos.'; + } // Validación de cantidad + if (opcion.cantidad === undefined || opcion.cantidad === null || opcion.cantidad === '') { + erroresOpcion.cantidad = 'La cantidad es obligatoria'; + } else { + const cantidadNum = Number(opcion.cantidad); + if (isNaN(cantidadNum) || cantidadNum <= 0) { + erroresOpcion.cantidad = 'La cantidad debe ser un número mayor a 0'; + } else if (cantidadNum > 9999999999) { + // 10 dígitos máximo (10^10 - 1) + erroresOpcion.cantidad = 'La cantidad no puede ser mayor a 9,999,999,999'; + } } // Validación de descuento @@ -62,13 +66,25 @@ export const validarVariantes = (variantes) => { if (!/^(0|[1-9]\d{0,4})(\.\d{1,2})?$/.test(opcion.descuento.toString())) { erroresOpcion.descuento = 'El descuento debe ser un número válido con máximo 5 dígitos.'; } - } - - // Validación de costo adicional con formato (10,2) - if (!Number.isFinite(opcion.costoAdicional) || opcion.costoAdicional < 0) { - erroresOpcion.costoAdicional = 'El costo adicional no es válido o el campo está vacío.'; - } else if (!/^\d{1,8}(\.\d{1,2})?$/.test(opcion.costoAdicional.toString())) { - erroresOpcion.costoAdicional = 'El costo adicional debe tener máximo 10 dígitos.'; + } // Validación de costo adicional con formato (10,2) + if ( + opcion.costoAdicional !== undefined + && opcion.costoAdicional !== null + && opcion.costoAdicional !== '' + ) { + const costoNum = Number(opcion.costoAdicional); + if (isNaN(costoNum)) { + erroresOpcion.costoAdicional = 'El costo adicional debe ser un número válido'; + } else if (costoNum < 0) { + erroresOpcion.costoAdicional = 'El costo adicional no puede ser negativo'; + } else { + const partes = costoNum.toString().split('.'); + if (partes[0] && partes[0].length > 8) { + erroresOpcion.costoAdicional = 'El costo adicional debe tener máximo 8 dígitos enteros'; + } else if (partes[1] && partes[1].length > 2) { + erroresOpcion.costoAdicional = 'El costo adicional debe tener máximo 2 decimales'; + } + } } if (!opcion.SKUautomatico?.trim()) { diff --git a/src/Vistas/Componentes/Organismos/Formularios/CamposActualizarProducto.jsx b/src/Vistas/Componentes/Organismos/Formularios/CamposActualizarProducto.jsx new file mode 100644 index 00000000..9f401e44 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Formularios/CamposActualizarProducto.jsx @@ -0,0 +1,434 @@ +//RF[29] Actualiza Producto - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF29] +import { memo } from 'react'; +import { Grid } from '@mui/material'; +import Texto from '@Atomos/Texto'; +import CampoTexto from '@Atomos/CampoTexto'; +import CampoSelect from '@Atomos/CampoSelect'; +import TarjetaAccion from '@Moleculas/TarjetaAccion'; +import TarjetaElementoAccion from '@Moleculas/TarjetaElementoAccion'; + +const CampoTextoFormulario = memo( + ({ + etiqueta, + nombre, + valor, + onChange, + helperText, + error, + placeholder, + tipo = 'text', + multilinea = false, + filas = 1, + maxLongitud = 100, + maxLongitudDescripcion = 300, + ...rest + }) => { + const limiteCaracteres = nombre === 'descripcion' ? maxLongitudDescripcion : maxLongitud; + + return ( + + { + const nuevoValor = evento.target.value.slice(0, limiteCaracteres); + onChange({ target: { name: nombre, value: nuevoValor } }); + }} + helperText={ + tipo === 'text' && limiteCaracteres && valor + ? `${valor.length}/${limiteCaracteres} - Máximo de caracteres. ${helperText || ''}` + : helperText + } + type={tipo} + size='medium' + required={true} + placeholder={placeholder} + multiline={multilinea} + rows={filas} + error={error} + inputProps={{ maxLength: tipo === 'text' ? limiteCaracteres : undefined }} + {...rest} + /> + + ); + } +); + +const CampoSelectFormulario = memo( + ({ + etiqueta, + nombre, + opciones, + valor, + onChange, + placeholder, + helperText, + error, + tamano = 6, + required = false, + }) => ( + + + + ) +); + +const TituloFormulario = memo(({ titulo, varianteTitulo, tamano = 12 }) => ( + + + {titulo} + + +)); + +const CampoImagenProducto = memo( + ({ imagenProducto, setImagenes, refInputArchivo, alAgregarImagenProducto, error }) => ( + <> + {imagenProducto ? ( + + setImagenes((prev) => ({ ...prev, imagenProducto: null }))} + tooltipEliminar='Eliminar' + borderColor={error ? 'error.main' : 'primary.light'} + backgroundColor={error ? 'error.lighter' : 'primary.lighter'} + iconColor={error ? 'error' : 'primary'} + iconSize='large' + textoVariant='caption' + tabIndex={0} + disabled={false} + /> + {error && ( + + {error} + + )} + + ) : ( + <> + {' '} + + Sube la Imagen Principal del Producto Aquí + + Formatos aceptados: JPG, JPEG, PNG - Tamaño máximo: 5MB + + + + refInputArchivo.current.click()} + hoverScale={false} + /> + + + + )} + + ) +); + +const CamposActualizarProducto = memo( + ({ + producto, + imagenProducto, + refInputArchivo, + erroresProducto, + listaProveedores, + alActualizarProducto, + alAgregarImagenProducto, + setImagenes, + prevenirNumerosNegativos, + prevenirNumerosNoDecimales, + }) => { + if (!producto) return null; + return ( + <> + + + + + + + + + + { + prevenirNumerosNegativos(evento); + if (evento.key === '.' || evento.key === ',') { + evento.preventDefault(); + } + }} + onInput={(evento) => { + const valor = evento.target.value; + if (valor === '' || /^\d+$/.test(valor)) { + evento.target.value = valor; + } else { + evento.target.value = valor.replace(/\D/g, ''); + } + }} + /> + { + const valor = evento.target.value; + if ((valor.match(/\./g) || []).length > 1) { + evento.target.value = valor.slice(0, -1); + } + if (valor && parseFloat(valor) < 1) { + evento.target.value = 1; + } + }} + /> + { + if (evento.target.value && evento.target.value < 1) { + evento.target.value = 1; + } + }} + /> + { + if (evento.target.value && evento.target.value < 1) { + evento.target.value = 1; + } + }} + />{' '} + { + const valor = evento.target.value; + // Limitar a 5 dígitos enteros y 2 decimales + if (valor) { + const partes = valor.split('.'); + if (partes[0] && partes[0].length > 5) { + // Si la parte entera tiene más de 5 dígitos, truncarla + partes[0] = partes[0].substring(0, 5); + evento.target.value = partes.join('.'); + } + // Si el número es mayor a 99999.99, establecerlo al máximo + if (parseFloat(valor) > 99999.99) { + evento.target.value = '99999.99'; + } + } + }} + />{' '} + { + // Validar antes de pasar al manejador general + const valor = evento.target.value; + + // Si está vacío o es un valor válido entre 0 y 100, actualizar + if (valor === '' || (parseFloat(valor) >= 0 && parseFloat(valor) <= 100)) { + // Formatear para asegurar que no exceda los límites + const valorFormateado = valor === '' ? '' : parseFloat(valor) > 100 ? '100' : valor; + + // Solo llamar al actualizador si el valor es válido + alActualizarProducto({ + target: { + name: 'descuento', + value: valorFormateado, + }, + }); + } + }} + placeholder='Ej: 10' + tipo='number' + required={false} + min={0} + max={100} + onKeyDown={prevenirNumerosNoDecimales} + /> + + {' '} + + + ); + } +); + +export default CamposActualizarProducto; diff --git a/src/Vistas/Componentes/Organismos/Formularios/CamposOpcion.jsx b/src/Vistas/Componentes/Organismos/Formularios/CamposOpcion.jsx index 6c73a0c5..ff6836c1 100644 --- a/src/Vistas/Componentes/Organismos/Formularios/CamposOpcion.jsx +++ b/src/Vistas/Componentes/Organismos/Formularios/CamposOpcion.jsx @@ -144,7 +144,7 @@ const CamposOpcion = memo( onChange={(evento) => manejarActualizarOpcion('valorOpcion', evento.target.value)} error={Boolean(errores?.valorOpcion)} helperText={errores?.valorOpcion || ''} - /> + />{' '} manejarActualizarOpcion('cantidad', evento.target.value)} placeholder='Ingresa la cantidad' error={Boolean(errores?.cantidad)} - helperText={errores?.cantidad || ''} + helperText={errores?.cantidad || 'Valor entero positivo (máx. 9,999,999,999)'} min={1} + max={9999999999} onKeyDown={(evento) => { prevenirNumerosNegativos(evento); }} onInput={(evento) => { // Solo permite números enteros positivos const valor = evento.target.value; - if (valor === '' || /^\d+$/.test(valor)) { - evento.target.value = valor; + if (valor === '') { + evento.target.value = ''; + } else if (/^\d+$/.test(valor)) { + // Convertir a número para verificar el rango + const num = Number(valor); + if (num > 9999999999) { + evento.target.value = '9999999999'; + } else if (valor.length > 10) { + evento.target.value = valor.slice(0, 10); + } } else { - evento.target.value = valor.replace(/\D/g, ''); + evento.target.value = valor.replace(/\D/g, '').slice(0, 10); } }} /> @@ -184,7 +193,7 @@ const CamposOpcion = memo( onChange={(evento) => manejarActualizarOpcion('SKUcomercial', evento.target.value)} error={Boolean(errores?.SKUcomercial)} helperText={errores?.SKUcomercial || ''} - /> + />{' '} manejarActualizarOpcion('costoAdicional', evento.target.value)} placeholder='Ingresa el costo adicional' - helperText={errores?.costoAdicional} // Consolidado con helperText - error={errores?.costoAdicional} + helperText={errores?.costoAdicional || 'Máximo 8 dígitos enteros y 2 decimales'} + error={Boolean(errores?.costoAdicional)} min={0} onKeyDown={prevenirNumerosNoDecimales} - /*onInput={(evento) => { - if (evento.target.value && evento.target.value < 1) { - evento.target.value = 1; + onInput={(evento) => { + const valor = evento.target.value; + if (valor) { + const partes = valor.split('.'); + // Limitar a 8 dígitos enteros + if (partes[0] && partes[0].length > 8) { + partes[0] = partes[0].substring(0, 8); + evento.target.value = partes.join('.'); + } + // Limitar a 2 decimales + if (partes[1] && partes[1].length > 2) { + partes[1] = partes[1].substring(0, 2); + evento.target.value = partes.join('.'); + } } - }}*/ - /> + }} + />{' '} manejarActualizarOpcion('descuento', evento.target.value)} placeholder='Ingresa el descuento' - helperText={errores?.descuento} + helperText={errores?.descuento || 'Valores entre 0 y 100'} error={errores?.descuento} min={0} + max={100} onKeyDown={prevenirNumerosNoDecimales} - /*onInput={(evento) => { - if (evento.target.value && evento.target.value < 1) { - evento.target.value = 1; + onInput={(evento) => { + const valor = evento.target.value; + if (valor) { + // Limitar a un valor máximo de 100 + if (parseFloat(valor) > 100) { + evento.target.value = '100'; + } + // Limitar a 2 decimales + const partes = valor.split('.'); + if (partes[1] && partes[1].length > 2) { + partes[1] = partes[1].substring(0, 2); + evento.target.value = partes.join('.'); + } } - }}*/ + }} /> ( + + + {titulo} + + +)); + +const CampoCrear = memo(({ etiqueta, onClick }) => ( + + + +)); + +const ContenidoFormulario = memo(() => { + const { + refInputArchivo, + variantes, + idsVariantes, + producto, + imagenes, + setImagenes, + erroresProducto, + erroresVariantes, + intentosEnviar, + listaProveedores, + manejarCrearVariante, + manejarActualizarVariante, + manejarEliminarVariante, + manejarAgregarOpcion, + manejarActualizarOpcion, + manejarEliminarOpcion, + manejarAgregarImagenVariante, + manejarEliminarImagenVariante, + manejarActualizarProducto, + manejarAgregarImagenProducto, + prevenirNumerosNegativos, + prevenirNumerosNoDecimales, + } = useProductoForm(); + return ( + <> + + {' '} + + + + {idsVariantes.map((idVariante) => ( + + ))} + + + + + ); +}); + +const FormularioActualizarProducto = memo( + ({ formularioAbierto, alCerrarFormularioProducto, detalleProducto }) => { + return ( + + + + ); + } +); + +const FormularioModal = memo( + ({ formularioAbierto, alCerrarFormularioProducto, detalleProducto }) => { + const { + manejarGuardarProductoActualizado, + alerta, + cargando, + setAlerta, + inicializarDatosProducto, + } = useProductoForm(); + + // Inicializar datos del producto cuando el formulario se abre + React.useEffect(() => { + if (formularioAbierto && detalleProducto) { + inicializarDatosProducto(detalleProducto); + } + }, [formularioAbierto, detalleProducto, inicializarDatosProducto]); + + // Efecto para manejar el cierre del modal cuando hay éxito en la actualización + React.useEffect(() => { + if (alerta && alerta.tipo === 'success' && alerta.mensaje?.includes('éxito')) { + // Mostrar el mensaje de éxito durante 1.5 segundos antes de cerrar el modal + const timerCierre = setTimeout(() => { + alCerrarFormularioProducto(); + }, 1500); + + // Limpiar el temporizador si el componente se desmonta o si cambia el estado de alerta + return () => clearTimeout(timerCierre); + } + }, [alerta, alCerrarFormularioProducto]); + + // Función para manejar el cierre manual del modal + const handleCloseModal = React.useCallback(() => { + // Limpiar cualquier alerta pendiente + setAlerta(null); + // Cerrar el modal + alCerrarFormularioProducto(); + }, [setAlerta, alCerrarFormularioProducto]); + + return ( + <> + setAlerta(null), + } + : null + } + > + + + + ); + } +); + +export default FormularioActualizarProducto; diff --git a/src/Vistas/Paginas/Productos/ListaProductos.jsx b/src/Vistas/Paginas/Productos/ListaProductos.jsx index dce165ea..c43db86b 100644 --- a/src/Vistas/Paginas/Productos/ListaProductos.jsx +++ b/src/Vistas/Paginas/Productos/ListaProductos.jsx @@ -10,6 +10,7 @@ import ContenedorLista from '@Organismos/ContenedorLista'; import Alerta from '@Moleculas/Alerta'; import PopUp from '@Moleculas/PopUp'; import FormularioProducto from '@Organismos/Formularios/FormularioProducto'; +import FormularioActualizarProducto from '@Organismos/Formularios/FormularioActualizarProducto'; import FormularioProveedor from '@Organismos/Formularios/FormularioProveedor'; import { useConsultarProductos } from '@Hooks/Productos/useConsultarProductos'; import { useEliminarProductos } from '@Hooks/Productos/useEliminarProductos'; @@ -38,13 +39,13 @@ const ListaProductos = () => { const [openModalEliminar, setAbrirPopUp] = useState(false); const [modalImportarAbierto, setModalImportarAbierto] = useState(false); const { importar, errores, exito, cargando: cargandoImportacion } = useImportarProductos(); - - - const [abrirModalDetalle, setAbrirModalDetalle] = useState(false); - const [imagenProducto, setImagenProducto] = useState('') + const [abrirModalDetalle, setAbrirModalDetalle] = useState(false); + const [imagenProducto, setImagenProducto] = useState(''); + const [mostrarModalActualizarProducto, setMostrarModalActualizarProducto] = useState(false); const [openModalExportar, setAbrirPopUpExportar] = useState(false); - const MENSAJE_POPUP_EXPORTAR = '¿Deseas exportar la lista de productos? El archivo será generado en formato .xlsx'; + const MENSAJE_POPUP_EXPORTAR + = '¿Deseas exportar la lista de productos? El archivo será generado en formato .xlsx'; const manejarCancelarExportar = () => { setAbrirPopUpExportar(false); }; @@ -66,7 +67,7 @@ const ListaProductos = () => { }; const { exportar, error: errorExportar, mensaje } = useExportarProductos(); - + useEffect(() => { if (errorExportar) { setAlerta({ @@ -108,6 +109,10 @@ const ListaProductos = () => { setMostrarModalProducto(true); setMostrarModalProveedor(false); }, []); + const mostrarFormularioActualizarProducto = useCallback(() => { + setMostrarModalActualizarProducto(true); + setAbrirModalDetalle(false); + }, []); const mostrarFormularioProveedor = useCallback(() => { setMostrarModalProducto(false); @@ -118,6 +123,10 @@ const ListaProductos = () => { setMostrarModalProducto(false); recargar(); }, [recargar]); + const cerrarFormularioActualizarProducto = useCallback(() => { + setMostrarModalActualizarProducto(false); + recargar(); + }, [recargar]); const cerrarFormularioProveedor = useCallback(() => { setMostrarModalProveedor(false); @@ -127,7 +136,6 @@ const ListaProductos = () => { const manejarCancelarEliminar = () => { setAbrirPopUp(false); }; - const manejarConfirmarEliminar = async () => { try { const urlsImagenes = productos @@ -194,10 +202,10 @@ const ListaProductos = () => { renderCell: ({ row: { estado } }) => ( @@ -229,7 +237,6 @@ const ListaProductos = () => { size: 'large', outlineColor: colores.altertex[1], disabled: !usuario?.permisos?.includes(PERMISOS.IMPORTAR_PRODUCTOS), - }, { variant: 'outlined', @@ -265,8 +272,8 @@ const ListaProductos = () => { return ( <> {mostrarModalProducto && ( @@ -276,16 +283,23 @@ const ListaProductos = () => { alMostrarFormularioProveedor={mostrarFormularioProveedor} /> )} + + {mostrarModalActualizarProducto && ( + + )} + {mostrarModalProveedor && ( )} - - {error && ( - - )} + + {error && } { checkboxSelection rowHeight={80} onRowSelectionModelChange={(nuevosIds) => { - const ids = (Array.isArray(nuevosIds) ? nuevosIds : Array.from(nuevosIds?.ids || [])) - .map(id => parseInt(id)); + const ids = ( + Array.isArray(nuevosIds) ? nuevosIds : Array.from(nuevosIds?.ids || []) + ).map((id) => parseInt(id)); setProductosSeleccionados(ids); }} onRowClick={(parametros) => { setProductoDetalleSeleccionado(parametros.row.id); - setImagenProducto(parametros.row.urlImagen) + setImagenProducto(parametros.row.urlImagen); setAbrirModalDetalle(true); }} /> @@ -323,9 +338,9 @@ const ListaProductos = () => { abrir={openModalEliminar} cerrar={manejarCancelarEliminar} confirmar={manejarConfirmarEliminar} - dialogo="¿Estás seguro de que deseas eliminar los productos seleccionados? Esta acción no se puede deshacer." + dialogo='¿Estás seguro de que deseas eliminar los productos seleccionados? Esta acción no se puede deshacer.' /> - setModalImportarAbierto(false)} onConfirm={importar} @@ -340,8 +355,8 @@ const ListaProductos = () => { cerrar={manejarCancelarExportar} confirmar={manejarConfirmarExportar} dialogo={MENSAJE_POPUP_EXPORTAR} - labelCancelar = 'Cancelar' - labelConfirmar = 'Confirmar' + labelCancelar='Cancelar' + labelConfirmar='Confirmar' disabledConfirmar={cargando} /> @@ -358,8 +373,8 @@ const ListaProductos = () => { variant: 'contained', color: 'primary', backgroundColor: colores.altertex[1], - onClick: () => console.log('Editar producto'), - construccion: true, + onClick: mostrarFormularioActualizarProducto, + disabled: !usuario?.permisos?.includes(PERMISOS.ACTUALIZAR_PRODUCTO), }, { label: 'Salir', @@ -375,7 +390,7 @@ const ListaProductos = () => { ) : errorDetalle ? (

Error al cargar la información del producto: {errorDetalle}

) : ( - + )} )} diff --git a/src/hooks/Empleados/useConsultarEmpleados.js b/src/hooks/Empleados/useConsultarEmpleados.js index 2b95ee3b..f1786b63 100644 --- a/src/hooks/Empleados/useConsultarEmpleados.js +++ b/src/hooks/Empleados/useConsultarEmpleados.js @@ -50,8 +50,8 @@ export function useConsultarEmpleados() { }, [usuario, recargarToken]); const recargar = () => { - setRecargarToken(prev => prev + 1); + setRecargarToken((prev) => prev + 1); }; return { empleados, mensaje, cargando, error, recargar }; -} \ No newline at end of file +} diff --git a/src/hooks/Empleados/useCrearEmpleado.js b/src/hooks/Empleados/useCrearEmpleado.js index 1129347e..edb25758 100644 --- a/src/hooks/Empleados/useCrearEmpleado.js +++ b/src/hooks/Empleados/useCrearEmpleado.js @@ -59,8 +59,8 @@ const validarDatosCrearEmpleado = (datos) => { } else if (!tieneNumero.test(datos.contrasenia)) { errores.contrasenia = 'Debe contener al menos un número'; } else if (datos.contrasenia.replace(/\s/g, '').length < 2) { - errores.contrasenia = - 'La contraseña no puede estar compuesta solo de espacios y un carácter especial'; + errores.contrasenia + = 'La contraseña no puede estar compuesta solo de espacios y un carácter especial'; } if (!datos.confirmarContrasenia || datos.confirmarContrasenia.trim() === '') { @@ -78,9 +78,9 @@ const validarDatosCrearEmpleado = (datos) => { errores.posicion = true; } if ( - !datos.cantidadPuntos || - isNaN(Number(datos.cantidadPuntos)) || - Number(datos.cantidadPuntos) < 0 + !datos.cantidadPuntos + || isNaN(Number(datos.cantidadPuntos)) + || Number(datos.cantidadPuntos) < 0 ) { errores.cantidadPuntos = 'La cantidad de puntos debe ser un número positivo'; } diff --git a/src/hooks/Productos/ProductoFormProvider.jsx b/src/hooks/Productos/ProductoFormProvider.jsx index 6526b713..d1f234fe 100644 --- a/src/hooks/Productos/ProductoFormProvider.jsx +++ b/src/hooks/Productos/ProductoFormProvider.jsx @@ -1,8 +1,11 @@ import { createContext, useContext, useState, useCallback, useMemo, useRef } from 'react'; import { useConsultarProveedores } from '@Hooks/Proveedores/useConsultarProveedores'; import { useCrearProducto } from '@Hooks/Productos/useCrearProducto'; +import { useActualizarProducto } from '@Hooks/Productos/useActualizarProducto'; import { useGenerarSKU } from '@Hooks/Productos/useGenerarSKU'; import { v4 as uuidv4 } from 'uuid'; +import { validarProducto } from '@Utilidades/Validaciones/validarProducto'; +import { validarVariantes } from '@Utilidades/Validaciones/validarVariantes'; const ProductoFormContext = createContext(); @@ -36,9 +39,8 @@ export const useProductoForm = () => { export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) => { const refInputArchivo = useRef(); - const [cargando, setCargando] = useState(false); - + const [intentosEnviar, setIntentosEnviar] = useState(0); const [alerta, setAlerta] = useState(null); const [variantes, setVariantes] = useState({ @@ -64,7 +66,7 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = precioVenta: undefined, costo: undefined, impuesto: 16, - descuento: 0, + descuento: undefined, estado: 1, envio: undefined, idProveedor: undefined, @@ -74,9 +76,13 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = imagenProducto: null, imagenesVariantes: {}, }); - const { proveedores } = useConsultarProveedores(); - const { erroresProducto, erroresVariantes, guardarProducto } = useCrearProducto(); + const { guardarProducto } = useCrearProducto(); + const { actualizarProducto } = useActualizarProducto(); + + // Estados para manejar los errores + const [erroresProducto, setErroresProducto] = useState({}); + const [erroresVariantes, setErroresVariantes] = useState({}); const generarSKUAutomatico = useGenerarSKU(); @@ -99,16 +105,19 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = setIdsVariantes((prev) => [...prev, nuevoId].sort((id1, id2) => id1 - id2)); - setImagenes((prev) => ({ - ...prev, - imagenesVariantes: { - ...prev.imagenesVariantes, - [nuevoId]: [], - }, - })); + setImagenes((prev) => { + const imagenesActualizadas = { ...prev }; + idsVariantes.forEach((id) => { + if (!imagenesActualizadas.imagenesVariantes[id]) { + imagenesActualizadas.imagenesVariantes[id] = []; + } + }); + return imagenesActualizadas; + }); }, [idsVariantes]); const manejarActualizarVariante = useCallback((idVariante, campo, valor) => { + // Primero actualizar el estado de las variantes setVariantes((prev) => { if (prev[idVariante] && prev[idVariante][campo] === valor) { return prev; @@ -121,8 +130,51 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = [campo]: valor, }, }; - }, []); - }); + }); + + // Luego, en una operación separada, validar y actualizar errores + setTimeout(() => { + setVariantes((variantesActuales) => { + if (!variantesActuales[idVariante]) return variantesActuales; + + // Validar solo la variante actualizada + const varianteParaValidar = { + [idVariante]: { + ...variantesActuales[idVariante], + identificador: idVariante, + }, + }; + + const validacionPartial = validarVariantes(varianteParaValidar); + + // Actualizar errores de la variante específica + setErroresVariantes((prevErrores) => { + const nuevosErrores = { ...prevErrores }; + + if (validacionPartial[idVariante] && validacionPartial[idVariante][campo]) { + // Si hay un error para este campo en esta variante + if (!nuevosErrores[idVariante]) { + nuevosErrores[idVariante] = {}; + } + nuevosErrores[idVariante][campo] = validacionPartial[idVariante][campo]; + } else if (nuevosErrores[idVariante]) { + // Si no hay error, eliminarlo + delete nuevosErrores[idVariante][campo]; + + // Si la variante no tiene más errores, eliminar la entrada + if (Object.keys(nuevosErrores[idVariante]).length === 0) { + delete nuevosErrores[idVariante]; + } + } + + return nuevosErrores; + }); + + // No modificar el estado, solo devolver el actual + return variantesActuales; + }); + }, 0); + }, []); const manejarEliminarVariante = useCallback((idVariante) => { setVariantes((prev) => { @@ -195,8 +247,8 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = valorOpcion: '', SKUautomatico: sku, SKUcomercial: '', - costoAdicional: 0, - descuento: 0, + costoAdicional: undefined, + descuento: undefined, estado: 1, }; @@ -211,7 +263,6 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = }, [generarSKUAutomatico, producto.nombreComun] ); - const manejarActualizarOpcion = useCallback( (idVariante, indiceOpcion, campo, valor) => { setVariantes((prev) => { @@ -219,7 +270,6 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = if (!varianteActual) return prev; const opcionesActuales = varianteActual.opciones || []; - if (indiceOpcion < 0 || indiceOpcion >= opcionesActuales.length) { return prev; } @@ -245,34 +295,92 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = }; } - return { + const nuevasVariantes = { ...prev, [idVariante]: { ...varianteActual, opciones: opcionesActualizadas, }, }; + + // Validar solo la variante actualizada + const varianteParaValidar = { + [idVariante]: { + ...nuevasVariantes[idVariante], + identificador: idVariante, + }, + }; + + const validacionPartial = validarVariantes(varianteParaValidar); + + // Actualizar errores de la opción específica + setErroresVariantes((prevErrores) => { + const nuevosErrores = { ...prevErrores }; + + if ( + validacionPartial[idVariante] + && validacionPartial[idVariante].opciones + && validacionPartial[idVariante].opciones[indiceOpcion] + && validacionPartial[idVariante].opciones[indiceOpcion][campo] + ) { + // Asegurarse de que existe la estructura para guardar el error + if (!nuevosErrores[idVariante]) { + nuevosErrores[idVariante] = { opciones: {} }; + } else if (!nuevosErrores[idVariante].opciones) { + nuevosErrores[idVariante].opciones = {}; + } + + if (!nuevosErrores[idVariante].opciones[indiceOpcion]) { + nuevosErrores[idVariante].opciones[indiceOpcion] = {}; + } + + // Guardar el error de este campo específico + nuevosErrores[idVariante].opciones[indiceOpcion][campo] + = validacionPartial[idVariante].opciones[indiceOpcion][campo]; + } else if ( + nuevosErrores[idVariante] + && nuevosErrores[idVariante].opciones + && nuevosErrores[idVariante].opciones[indiceOpcion] + ) { + // Eliminar el error si ya no existe + delete nuevosErrores[idVariante].opciones[indiceOpcion][campo]; + + // Limpiar la estructura si ya no hay errores + if (Object.keys(nuevosErrores[idVariante].opciones[indiceOpcion]).length === 0) { + delete nuevosErrores[idVariante].opciones[indiceOpcion]; + } + + if (Object.keys(nuevosErrores[idVariante].opciones).length === 0) { + delete nuevosErrores[idVariante].opciones; + } + + if (Object.keys(nuevosErrores[idVariante]).length === 0) { + delete nuevosErrores[idVariante]; + } + } + + return nuevosErrores; + }); + + return nuevasVariantes; }); }, [generarSKUAutomatico, producto.nombreComun] ); - const manejarEliminarOpcion = useCallback((idVariante, indiceOpcion) => { - setVariantes((prev) => { - const varianteActual = prev[idVariante]; + setVariantes((variantesActuales) => { + const varianteActual = variantesActuales[idVariante]; // prettier-ignore if ( !varianteActual || !varianteActual.opciones || indiceOpcion >= varianteActual.opciones.length ) { - return prev; + return variantesActuales; } - - const opcionesActualizadas = [ - ...varianteActual.opciones.slice(0, indiceOpcion), - ...varianteActual.opciones.slice(indiceOpcion + 1), - ]; + const opcionesActualizadas = varianteActual.opciones.filter( + (opcion, index) => index !== indiceOpcion + ); setAlerta({ tipo: 'success', @@ -280,31 +388,343 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = }); return { - ...prev, + ...variantesActuales, [idVariante]: { ...varianteActual, opciones: opcionesActualizadas, }, }; }); - }, []); - const manejarActualizarProducto = useCallback((evento) => { - const { name, value } = evento.target; - setProducto((prev) => ({ ...prev, [name]: value })); - }, []); + // Also clear any errors associated with the removed option + setErroresVariantes((erroresPrevios) => { + if (!erroresPrevios[idVariante]?.opciones?.[indiceOpcion]) { + return erroresPrevios; + } + + const nuevosErrores = { ...erroresPrevios }; + + if (nuevosErrores[idVariante]?.opciones) { + const opcionesActualizadas = { ...nuevosErrores[idVariante].opciones }; + delete opcionesActualizadas[indiceOpcion]; + + if (Object.keys(opcionesActualizadas).length === 0) { + delete nuevosErrores[idVariante].opciones; + if (Object.keys(nuevosErrores[idVariante]).length === 0) { + delete nuevosErrores[idVariante]; + } + } else { + nuevosErrores[idVariante].opciones = opcionesActualizadas; + } + } + + return nuevosErrores; + }); + }, []); // Esta función se usa para actualizar y validar un campo individual del producto + const manejarActualizarProducto = useCallback( + (evento) => { + const { name, value } = evento.target; + + setProducto((prev) => { + return { ...prev, [name]: value }; + }); + + requestAnimationFrame(() => { + // Validar solo el campo que se está actualizando + const campoParaValidar = { [name]: value }; + const validacionPartial = validarProducto({ ...producto, ...campoParaValidar }); + + // Actualizar solo el error del campo específico + setErroresProducto((erroresActuales) => { + const nuevosErrores = { ...erroresActuales }; + + // Si hay un error para este campo, actualizarlo + if (validacionPartial[name]) { + nuevosErrores[name] = validacionPartial[name]; + } else { + // Si no hay error, eliminarlo de la lista de errores + delete nuevosErrores[name]; + } + + return nuevosErrores; + }); + }); + }, + [producto] + ); + + const manejarGuardarProductoActualizado = useCallback(async () => { + try { + setCargando(true); + if (!producto.idProducto) { + setAlerta({ + tipo: 'error', + mensaje: + 'El ID del producto es requerido para actualizar. Por favor, asegúrate de seleccionar un producto válido.', + }); + setCargando(false); + return; + } + + const tamanioMaximoBytes = 5 * 1024 * 1024; + const erroresImagenes = {}; + + // Validar imagen principal + if (imagenes.imagenProducto && imagenes.imagenProducto.size > tamanioMaximoBytes) { + erroresImagenes.imagenProducto + = 'La imagen es demasiado grande. El tamaño máximo permitido es 5MB.'; + } + + // Validar imágenes de variantes + Object.entries(imagenes.imagenesVariantes).forEach(([idVariante, imagenesVariante]) => { + const imagenesGrandes = imagenesVariante.filter( + (img) => img.file && img.file.size > tamanioMaximoBytes + ); + if (imagenesGrandes.length > 0) { + if (!erroresImagenes.variantes) erroresImagenes.variantes = {}; + erroresImagenes.variantes[ + idVariante + ] = `${imagenesGrandes.length} imagen(es) excede(n) el tamaño máximo de 5MB.`; + } + }); + + setAlerta({ + tipo: 'info', + mensaje: 'Validando datos del producto...', + }); + + // Validar datos del producto + const erroresValidacionProducto = validarProducto(producto); + + // Validar datos de las variantes + const erroresValidacionVariantes = validarVariantes(variantes); + + // Combinar todos los errores + const hayErroresImagenes = Object.keys(erroresImagenes).length > 0; + const hayErroresProducto = Object.keys(erroresValidacionProducto).length > 0; + const hayErroresVariantes = Object.keys(erroresValidacionVariantes).length > 0; + + // Actualizar estados de error + setErroresProducto({ + ...erroresValidacionProducto, + ...(erroresImagenes.imagenProducto && { imagenProducto: erroresImagenes.imagenProducto }), + }); + + setErroresVariantes(() => ({ + ...erroresValidacionVariantes, + ...(erroresImagenes.variantes || {}), + })); + + // Si hay errores, mostrar mensaje y detener el proceso + if (hayErroresImagenes || hayErroresProducto || hayErroresVariantes) { + setIntentosEnviar((prev) => prev + 1); + + let mensajeError = 'Por favor revisa los siguientes campos:'; + + if (hayErroresProducto || erroresImagenes.imagenProducto) { + const camposConErrores = [ + ...Object.keys(erroresValidacionProducto), + ...(erroresImagenes.imagenProducto ? ['imagen principal'] : []), + ].join(', '); + mensajeError += `\n- Datos del producto: ${camposConErrores}`; + } + + if (hayErroresVariantes || erroresImagenes.variantes) { + mensajeError += '\n- Hay errores en una o más variantes y sus opciones'; + } + + setAlerta({ + tipo: 'error', + mensaje: mensajeError, + }); + setCargando(false); + return; + } // No se valida imagen para actualización, el backend usa la existente si no hay nueva + // La validación de imagen solo aplica para productos nuevos + + const variantesArray = Object.entries(variantes).map(([idVariante, datos]) => ({ + identificador: idVariante, + ...datos, + })); + + if (!variantesArray.length) { + setAlerta({ + tipo: 'error', + mensaje: 'Debes agregar al menos una variante al producto.', + }); + setCargando(false); + return; + } + + const variantesSinOpciones = variantesArray.filter( + (variante) => !Array.isArray(variante.opciones) || variante.opciones.length === 0 + ); + + if (variantesSinOpciones.length > 0) { + setAlerta({ + tipo: 'error', + mensaje: 'Cada variante debe incluir al menos una opción disponible.', + }); + setCargando(false); + return; + } // Si llegamos aquí, todo está validado. Proceder con la actualización + setAlerta({ + tipo: 'info', + mensaje: 'Actualizando producto...', + }); + + try { + // Formateamos los datos para la actualización + const datosVariantes = Object.entries(variantes).map(([id, datos]) => ({ + identificador: id, + nombreVariante: datos.nombreVariante, + descripcion: datos.descripcion, + opciones: datos.opciones.map((opcion) => ({ + cantidad: parsearNumero(opcion.cantidad), + valorOpcion: opcion.valorOpcion, + SKUautomatico: opcion.SKUautomatico || '', + SKUcomercial: opcion.SKUcomercial || '', + costoAdicional: parsearNumero(opcion.costoAdicional), + descuento: parsearNumero(opcion.descuento), + estado: Number(opcion.estado) || 1, + })), + })); + + const productoFormateado = { + ...producto, + idProducto: producto.idProducto, // Ensure idProducto is included + precioPuntos: parsearNumero(producto.precioPuntos), + precioCliente: parsearNumero(producto.precioCliente), + precioVenta: parsearNumero(producto.precioVenta), + costo: parsearNumero(producto.costo), + impuesto: parsearNumero(producto.impuesto), + descuento: parsearNumero(producto.descuento), + estado: Number(producto.estado) || 1, + envio: parsearNumero(producto.envio), + idProveedor: parsearNumero(producto.idProveedor), + }; + + // Llamada a la API para actualizar el producto + const resultado = await actualizarProducto({ + productoRaw: productoFormateado, + variantesRaw: datosVariantes, + imagenProducto: imagenes.imagenProducto, + imagenesVariantes: imagenes.imagenesVariantes, + }); + + // Si la actualización fue exitosa + if (resultado?.exito) { + setAlerta({ + tipo: 'success', + mensaje: 'Producto actualizado con éxito', + }); + + // Después de un breve retraso, cerrar el formulario + setTimeout(() => { + alCerrarFormularioProducto(); + }, 2000); + } else if (resultado?.mensaje) { + setAlerta({ + tipo: 'error', + mensaje: resultado.mensaje, + }); + } else { + setAlerta({ + tipo: 'success', + mensaje: 'Producto actualizado con éxito', + }); + + // Después de un breve retraso, cerrar el formulario + setTimeout(() => { + alCerrarFormularioProducto(); + }, 2000); + } + + // No cerramos el modal aquí, dejamos que el useEffect en FormularioModal se encargue + // después de mostrar el mensaje de éxito por un momento + } catch (error) { + console.error('Error en la comunicación con el servidor:', error); + setAlerta({ + tipo: 'error', + mensaje: 'Error al comunicarse con el servidor', + }); + } + } catch (error) { + console.error('Error al actualizar producto:', error); + setAlerta({ + tipo: 'error', + mensaje: `Ocurrió un error al actualizar el producto: ${ + error.message || 'Error desconocido' + }`, + }); + } finally { + setCargando(false); + } + }, [producto, variantes, imagenes, alCerrarFormularioProducto, actualizarProducto]); const manejarAgregarImagenVariante = useCallback( (idVariante, archivos) => { + // Tamaño máximo permitido: 5MB (5 * 1024 * 1024 bytes) + const tamanioMaximoBytes = 5 * 1024 * 1024; + + // Verificar cada archivo por tamaño + const archivosSobredimensionados = archivos.filter( + (archivo) => archivo.size > tamanioMaximoBytes + ); + + if (archivosSobredimensionados.length > 0) { + const mensajeError + = archivosSobredimensionados.length > 1 + ? `${archivosSobredimensionados.length} imágenes exceden el tamaño máximo de 5MB.` + : 'La imagen es demasiado grande. El tamaño máximo permitido es 5MB.'; + + setAlerta({ + tipo: 'error', + mensaje: mensajeError, + }); + + // Añadir error a la variante específica + setErroresVariantes((prevErrores) => { + const nuevosErrores = { ...prevErrores }; + if (!nuevosErrores[idVariante]) { + nuevosErrores[idVariante] = {}; + } + nuevosErrores[idVariante].imagenes = mensajeError; + return nuevosErrores; + }); + + // Si hay archivos válidos, continuamos con ellos + if (archivosSobredimensionados.length === archivos.length) { + return; // Si todos los archivos exceden el tamaño, no hacemos nada + } + } else { + // Limpiar errores de imágenes para esta variante si todos los archivos son válidos + setErroresVariantes((prevErrores) => { + const nuevosErrores = { ...prevErrores }; + if (nuevosErrores[idVariante]?.imagenes) { + delete nuevosErrores[idVariante].imagenes; + if (Object.keys(nuevosErrores[idVariante]).length === 0) { + delete nuevosErrores[idVariante]; + } + } + return nuevosErrores; + }); + } + + // Filtrar solo los archivos que no exceden el tamaño + const archivosValidos = archivos.filter((archivo) => archivo.size <= tamanioMaximoBytes); + + if (archivosValidos.length === 0) return; + setImagenes((prev) => { const imagenesVariante = prev.imagenesVariantes[idVariante] || []; - const nuevasImagenes = archivos.map((archivo) => ({ + const nuevasImagenes = archivosValidos.map((archivo) => ({ id: `${archivo.name}_${uuidv4()}_${idVariante}`, idVariante, file: archivo, })); - setSiguienteIdImagen(siguienteIdImagen + archivos.length); + setSiguienteIdImagen(siguienteIdImagen + archivosValidos.length); return { ...prev, @@ -318,7 +738,9 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = setAlerta({ tipo: 'success', mensaje: `${ - archivos.length > 1 ? `${archivos.length} imágenes agregadas` : 'Imagen agregada' + archivosValidos.length > 1 + ? `${archivosValidos.length} imágenes agregadas` + : 'Imagen agregada' } a la variante`, }); }, @@ -455,11 +877,38 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = setCargando(false); } }, [guardarProducto, imagenes, producto, variantes, alCerrarFormularioProducto]); - const manejarAgregarImagenProducto = useCallback((evento) => { const archivo = evento.target.files[0]; if (!archivo) return; + // Tamaño máximo permitido: 5MB (5 * 1024 * 1024 bytes) + const tamanioMaximoBytes = 5 * 1024 * 1024; + + if (archivo.size > tamanioMaximoBytes) { + // Mostrar alerta de error y no actualizar la imagen + setAlerta({ + tipo: 'error', + mensaje: 'La imagen es demasiado grande. El tamaño máximo permitido es 5MB.', + }); + + // Añadir error específico para la imagen + setErroresProducto((erroresActuales) => ({ + ...erroresActuales, + imagenProducto: 'La imagen es demasiado grande. El tamaño máximo permitido es 5MB.', + })); + + // No actualizar la imagen si excede el tamaño + evento.target.value = ''; + return; + } + + // Limpiar cualquier error de imagen anterior + setErroresProducto((erroresActuales) => { + const nuevosErrores = { ...erroresActuales }; + delete nuevosErrores.imagenProducto; + return nuevosErrores; + }); + setImagenes((prev) => ({ ...prev, imagenProducto: archivo })); setAlerta({ @@ -476,10 +925,9 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = })) || [], [proveedores] ); - useMemo(() => { - setImagenes((prev) => { - const imagenesActualizadas = { ...prev }; + setImagenes((imagenesActuales) => { + const imagenesActualizadas = { ...imagenesActuales }; idsVariantes.forEach((id) => { if (!imagenesActualizadas.imagenesVariantes[id]) { @@ -489,8 +937,97 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = return imagenesActualizadas; }); - }, [idsVariantes]); + }, [idsVariantes]); // Función para inicializar los datos del formulario a partir de detalleProducto + const inicializarDatosProducto = useCallback( + (detalleProducto) => { + if (!detalleProducto) return; + + // Resetear estados de errores al inicializar un producto + setErroresProducto({}); + setErroresVariantes({}); + setIntentosEnviar(0); + + // Inicializar datos básicos del producto + setProducto({ + idProducto: detalleProducto.idProducto, // Ensure idProducto is included + nombreComun: detalleProducto.nombreComun || '', + nombreComercial: detalleProducto.nombreComercial || '', + descripcion: detalleProducto.descripcion || '', + marca: detalleProducto.marca || '', + modelo: detalleProducto.modelo || '', + tipoProducto: detalleProducto.tipoProducto || '', + precioPuntos: detalleProducto.precioPuntos || undefined, + precioCliente: detalleProducto.precioCliente || undefined, + precioVenta: detalleProducto.precioVenta || undefined, + costo: detalleProducto.costo || undefined, + impuesto: detalleProducto.impuesto || 16, + descuento: detalleProducto.descuento || undefined, + estado: detalleProducto.estado || 1, + envio: detalleProducto.envio || 1, + idProveedor: detalleProducto.idProveedor || undefined, + }); + + // Inicializar las variantes si existen + if (detalleProducto.variantes && detalleProducto.variantes.length > 0) { + // Transformar el arreglo de variantes a un objeto con idVariante como clave + const variantesObj = {}; + const varianteIds = []; + + detalleProducto.variantes.forEach((variante) => { + const idVariante = variante.idVariante; + varianteIds.push(idVariante); + + variantesObj[idVariante] = { + nombreVariante: variante.nombreVariante || '', + descripcion: variante.descripcion || '', + opciones: + variante.opciones?.map((opcion) => ({ + id: Date.now() + Math.random(), + cantidad: opcion.cantidad || 0, + valorOpcion: opcion.valorOpcion || '', + SKUautomatico: opcion.SKUautomatico || '', + SKUcomercial: opcion.SKUcomercial || '', + costoAdicional: opcion.costoAdicional || undefined, + descuento: opcion.descuento || undefined, + estado: opcion.estado || 1, + })) || [], + }; + }); + setVariantes(variantesObj); + setIdsVariantes(varianteIds); + + // Inicializar el objeto de imágenes para cada variante + const nuevasImagenes = { + imagenProducto: detalleProducto.imagenProducto || null, + imagenesVariantes: {}, + }; + + varianteIds.forEach((id) => { + nuevasImagenes.imagenesVariantes[id] = []; + }); + + setImagenes(nuevasImagenes); + } else { + // Si no hay variantes, inicializar con una variante vacía + setVariantes({ + 1: { + nombreVariante: '', + descripcion: '', + opciones: [], + }, + }); + + setIdsVariantes([1]); + + setImagenes({ + imagenProducto: null, + imagenesVariantes: { 1: [] }, + }); + } + }, + [setErroresProducto, setErroresVariantes, setIntentosEnviar] + ); const contextValue = { refInputArchivo, alerta, @@ -502,6 +1039,7 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = setImagenes, erroresProducto, erroresVariantes, + intentosEnviar, listaProveedores, cargando, manejarCrearVariante, @@ -514,9 +1052,11 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = manejarEliminarImagenVariante, manejarCrearProducto, manejarActualizarProducto, + manejarGuardarProductoActualizado, manejarAgregarImagenProducto, prevenirNumerosNegativos, prevenirNumerosNoDecimales, + inicializarDatosProducto, }; return ( diff --git a/src/hooks/Productos/useActualizarProducto.js b/src/hooks/Productos/useActualizarProducto.js new file mode 100644 index 00000000..d93f335c --- /dev/null +++ b/src/hooks/Productos/useActualizarProducto.js @@ -0,0 +1,56 @@ +//RF [29] Actualiza Producto - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF29] +import { useState } from 'react'; +import { RepositorioActualizarProducto } from '@Repositorios/Productos/RepositorioActualizarProducto'; +import { validarProducto } from '@Utilidades/Validaciones/validarProducto'; +import { validarVariantes } from '@Utilidades/Validaciones/validarVariantes'; +const MAX_IMAGE_SIZE = 5 * 1024 * 1024; + +export const useActualizarProducto = () => { + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(null); + + const actualizarProducto = async (datosProducto) => { + setCargando(true); + setError(null); + + try { + // Validar producto + const erroresProducto = validarProducto(datosProducto.productoRaw); + if (Object.keys(erroresProducto).length > 0) { + setError(erroresProducto); + setCargando(false); + return { exito: false, mensaje: 'Hay errores en los datos del producto' }; + } + + // Validar variantes - necesitamos formatear los datos primero + const variantesConIdentificador = {}; + datosProducto.variantesRaw.forEach((variante) => { + variantesConIdentificador[variante.identificador] = { + ...variante, + identificador: variante.identificador, + }; + }); + + const erroresVariantes = validarVariantes(variantesConIdentificador); + if (Object.keys(erroresVariantes).length > 0) { + setError(erroresVariantes); + setCargando(false); + return { exito: false, mensaje: 'Hay errores en los datos de las variantes' }; + } + + // Llamar al repositorio para actualizar el producto + const respuesta = await RepositorioActualizarProducto.actualizarProducto(datosProducto); + setCargando(false); + return { ...respuesta, exito: true }; + } catch (error) { + setError(error.message); + setCargando(false); + return { exito: false, mensaje: error.message || 'Error al actualizar el producto' }; + } + }; + return { + cargando, + error, + actualizarProducto, + }; +}; diff --git a/src/hooks/Productos/useLeerProducto.js b/src/hooks/Productos/useLeerProducto.js index f3b146d2..4410aa32 100644 --- a/src/hooks/Productos/useLeerProducto.js +++ b/src/hooks/Productos/useLeerProducto.js @@ -2,29 +2,30 @@ import { useEffect, useState } from 'react'; import { RepositorioLeerProducto } from '@Repositorios/Productos/RepositorioLeerProducto.js'; export const useLeerProducto = (idProducto) => { - const [detalleProducto, setDetalleProducto] = useState(null) - const [cargando, setCargando] =useState(true) - const [error, setError] = useState(null) + const [detalleProducto, setDetalleProducto] = useState(null); + const [cargando, setCargando] = useState(true); + const [error, setError] = useState(null); useEffect(() => { const obtenerInfoProducto = async () => { - setCargando(true) - setError(null) + setCargando(true); + setError(null); - try{ + try { const productoInfo = await RepositorioLeerProducto.obtenerPorId(idProducto); - setDetalleProducto(productoInfo) - }catch (err) { - setError(err.message) - }finally { - setCargando(false) + console.log('Producto obtenido:', productoInfo); + setDetalleProducto(productoInfo); + } catch (err) { + setError(err.message); + } finally { + setCargando(false); } - } + }; - if(idProducto) { - obtenerInfoProducto() + if (idProducto) { + obtenerInfoProducto(); } }, [idProducto]); - return {detalleProducto, cargando, error} -} \ No newline at end of file + return { detalleProducto, cargando, error }; +};