diff --git a/.github/workflows/on-pr.yaml b/.github/workflows/on-pr.yaml index 70bf8d2f..9b952562 100644 --- a/.github/workflows/on-pr.yaml +++ b/.github/workflows/on-pr.yaml @@ -6,6 +6,7 @@ on: - main - staging - develop + - MBI-1 jobs: lint: @@ -18,10 +19,8 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '22.14' - - name: Install dependencies run: npm install @@ -38,13 +37,11 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '22.14' - name: Install dependencies run: npm install - - name: Run Build run: npm run build diff --git a/public/ejemplo_importar_productos.csv b/public/ejemplo_importar_productos.csv new file mode 100644 index 00000000..31627a64 --- /dev/null +++ b/public/ejemplo_importar_productos.csv @@ -0,0 +1,26 @@ +idProducto,idProveedor,nombreProducto,nombreComercial,descripcionProducto,tipoProducto,marca,modelo,costo,precioVenta,precioCliente,precioPuntos,impuesto,descuento,estado,envio,nombreVariante,descripcionVariante,valorOpcion,SKUcomercial,cantidad,costoAdicional,descuentoOpcion,estadoOpcion +P1,101,Próducto 1,Comercial 1,Descripcion del producto 2,GENERAL,Marca1,M1,100,296.33,221.59,1,16,0,1,1,Variante 1,Variante 1 del producto 1,Opcion1,SKU-COM-1-1-1,1,0,0,1 +P1,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 1,Opcion2,SKU-COM-1-1-2,17,0,0,1 +P1,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 1,Opcion3,SKU-COM-1-1-3,17,0,0,1 +P1,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 1,Opcion4,SKU-COM-1-1-4,17,0,0,1 +P1,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 1,Opcion5,SKU-COM-1-1-5,8,0,0,1 +P1,,,,,,,,,,,,,,,,Variante 2,Variante 2 del producto 1,Opcion1,SKU-COM-1-2-1,14,20,0,1 +P1,,,,,,,,,,,,,,,,Variante 2,Variante 2 del producto 1,Opcion2,SKU-COM-1-2-2,11,20,0,1 +P1,,,,,,,,,,,,,,,,Variante 2,Variante 2 del producto 1,Opcion3,SKU-COM-1-2-3,13,20,0,1 +P1,,,,,,,,,,,,,,,,Variante 2,Variante 2 del producto 1,Opcion4,SKU-COM-1-2-4,12,20,0,1 +P1,,,,,,,,,,,,,,,,Variante 2,Variante 2 del producto 1,Opcion5,SKU-COM-1-2-5,12,20,0,1 +P2,101,Producto 2,Comercial 2,Descripcion del producto 11,GENERAL,Marca2,M2,56.48,229.2,190.15,12,16,0,1,1,Variante 1,Variante 1 del producto 2,Opcion1,SKU-COM-2-1-1,7,0,0,1 +P2,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 2,Opcion2,SKU-COM-2-1-2,11,0,0,1 +P2,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 2,Opcion3,SKU-COM-2-1-3,13,0,0,1 +P2,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 2,Opcion4,SKU-COM-2-1-4,17,0,0,1 +P2,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 2,Opcion5,SKU-COM-2-1-5,6,0,0,1 +P2,,,,,,,,,,,,,,,,Variante 2,Variante 2 del producto 2,Opcion1,SKU-COM-2-2-1,18,10,0,1 +P2,,,,,,,,,,,,,,,,Variante 2,Variante 2 del producto 2,Opcion2,SKU-COM-2-2-2,5,10,0,1 +P2,,,,,,,,,,,,,,,,Variante 2,Variante 2 del producto 2,Opcion3,SKU-COM-2-2-3,13,10,0,1 +P2,,,,,,,,,,,,,,,,Variante 2,Variante 2 del producto 2,Opcion4,SKU-COM-2-2-4,18,10,0,1 +P2,,,,,,,,,,,,,,,,Variante 2,Variante 2 del producto 2,Opcion5,SKU-COM-2-2-5,11,10,0,1 +P3,101,Producto 3,Comercial 3,Descripcion del producto 22,GENERAL,Marca3,M3,80.76,289.09,186.15,45,16,0,1,1,Variante 1,Variante 1 del producto 3,Opcion1,SKU-COM-3-1-1,18,0,0,1 +P3,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 3,Opcion2,SKU-COM-3-1-2,18,0,0,1 +P3,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 3,Opcion3,SKU-COM-3-1-3,18,0,0,1 +P3,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 3,Opcion4,SKU-COM-3-1-4,14,0,0,1 +P3,,,,,,,,,,,,,,,,Variante 1,Variante 1 del producto 3,Opcion5,SKU-COM-3-1-5,11,0,0,1 diff --git a/public/plantilla_importar_productos.csv b/public/plantilla_importar_productos.csv new file mode 100644 index 00000000..33c5db34 --- /dev/null +++ b/public/plantilla_importar_productos.csv @@ -0,0 +1 @@ +idProducto,idProveedor,nombreProducto,nombreComercial,descripcionProducto,tipoProducto,marca,modelo,costo,precioVenta,precioCliente,precioPuntos,impuesto,descuento,estado,envio,nombreVariante,descripcionVariante,valorOpcion,SKUautomatico,SKUcomercial,cantidad,costoAdicional,descuentoOpcion,estadoOpcion diff --git a/src/App.css b/src/App.css index b74d403b..7b0f5769 100644 --- a/src/App.css +++ b/src/App.css @@ -38,3 +38,7 @@ */ #root { text-align: center; } + +.css-1sf6n0n-MuiDataGrid-root { + height: auto !important; +} diff --git a/src/Dominio/Modelos/Cuotas/LeerCuota.js b/src/Dominio/Modelos/Cuotas/LeerCuota.js new file mode 100644 index 00000000..cb804bc7 --- /dev/null +++ b/src/Dominio/Modelos/Cuotas/LeerCuota.js @@ -0,0 +1,14 @@ +/* +Modelo de dominio para la lectura de una cuota. +RF[33] - Leer cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF33] +*/ + +export class LeerCuota { + constructor({ idCuota, nombre, descripcion, productos = [], cuotas = [] }) { + this.idCuota = idCuota; + this.nombre = nombre; + this.descripcion = descripcion; + this.productos = productos; + this.cuotas = cuotas; + } +} diff --git a/src/Dominio/Modelos/Empleados/CrearEmpleado.js b/src/Dominio/Modelos/Empleados/CrearEmpleado.js new file mode 100644 index 00000000..b2982c6b --- /dev/null +++ b/src/Dominio/Modelos/Empleados/CrearEmpleado.js @@ -0,0 +1,114 @@ +//RF16 - Crear empleado - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF16 + +/** + * Valida los datos del formulario para crear un empleado. + * Devuelve un objeto con los campos que tienen errores. + * @param {Object} datos - Datos del empleado + * @returns {Object} errores - Campos con error + */ + +export const validarDatosCrearEmpleado = (datos, empleadosExistentes = []) => { + const errores = {}; + + if (!datos.nombreCompleto || datos.nombreCompleto.trim() === '') { + errores.nombreCompleto = 'El campo es obligatorio'; + } + + if (!datos.fechaNacimiento) { + errores.fechaNacimiento = 'El campo es obligatorio'; + } else { + const hoy = new Date(); + const fecha = new Date(datos.fechaNacimiento); + + if (fecha > hoy) { + errores.fechaNacimiento = 'La fecha no puede ser futura'; + } else { + // Calcular la fecha mínima permitida (18 años antes de hoy) + const fechaMinima = new Date(hoy.getFullYear() - 18, hoy.getMonth(), hoy.getDate()); + if (fecha > fechaMinima) { + errores.fechaNacimiento = 'El empleado debe tener al menos 18 años al día de hoy'; + } + } + } + + const tieneCaracterEspecial = /[!@#$%^&*(),.?":{}|<>]/; + const tieneMayuscula = /[A-Z]/; + + if (!datos.genero) errores.genero = true; + + const correoValido = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!datos.correoElectronico) { + errores.correoElectronico = true; + } else if (!correoValido.test(datos.correoElectronico)) { + errores.correoElectronico = 'Correo electrónico no válido'; + } + const telefonoValido = /^\d{10}$/; + + if (!datos.numeroTelefono) { + errores.numeroTelefono = true; + } else if (!telefonoValido.test(datos.numeroTelefono)) { + errores.numeroTelefono = 'El número de teléfono debe tener exactamente 10 dígitos'; + } else if (empleadosExistentes.some((empleado) => empleado.telefono === datos.numeroTelefono)) { + errores.numeroTelefono = 'Este número ya está registrado'; + } + + if (!datos.direccion || datos.direccion.trim() === '') { + errores.direccion = true; + } + + if (!datos.contrasenia || datos.contrasenia.trim() === '') { + errores.contrasenia = true; + } else { + const contraseniaSinEspacios = datos.contrasenia.replace(/\s/g, ''); + if (datos.contrasenia.length < 8) { + errores.contrasenia = 'La contraseña debe tener al menos 8 caracteres'; + } else if (!tieneCaracterEspecial.test(datos.contrasenia)) { + errores.contrasenia + = 'Debe contener al menos uno de estos caracteres: ! @ # $ % ^ & * ( ) , . ? " : { } | < >'; + } else if (contraseniaSinEspacios.length < 2) { + errores.contrasenia + = 'La contraseña no puede estar compuesta solo de espacios y un carácter especial'; + } else if (!tieneMayuscula.test(datos.contrasenia)) { + errores.contrasenia = 'Debe contener al menos una letra mayúscula'; + } + } + if (!datos.confirmarContrasenia || datos.confirmarContrasenia.trim() === '') { + errores.confirmarContrasenia = true; + } else if (datos.contrasenia !== datos.confirmarContrasenia) { + errores.confirmarContrasenia = 'Las contraseñas no coinciden'; + } + + if (!datos.numeroEmergencia || datos.numeroEmergencia.trim() === '') { + errores.numeroEmergencia = true; + } else if (!telefonoValido.test(datos.numeroEmergencia)) { + errores.numeroEmergencia = 'El número de emergencia debe tener exactamente 10 dígitos'; + } + if (!datos.areaTrabajo || datos.areaTrabajo.trim() === '') { + errores.areaTrabajo = 'El campo no puede estar vacío ni contener sólo espacios'; + } + if (!datos.areaTrabajo || datos.areaTrabajo.trim() === '') { + errores.areaTrabajo = true; + } + if (!datos.posicion || datos.posicion.trim() === '') { + errores.posicion = 'El campo no puede estar vacío ni contener sólo espacios'; + } + + if (!datos.cantidadPuntos || isNaN(datos.cantidadPuntos) || datos.cantidadPuntos < 0) { + errores.cantidadPuntos = 'La cantidad de puntos debe ser un número positivo'; + } + + if (!datos.antiguedad) { + errores.antiguedad = 'La antigüedad es requerida'; + } else { + const hoy = new Date(); + const antiguedadFecha = new Date(datos.antiguedad); + if (antiguedadFecha > hoy) { + errores.antiguedad = 'La antigüedad no puede ser una fecha futura'; + } + } + if (!datos.antiguedad) { + errores.antiguedad = true; + } + + return errores; +}; diff --git a/src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js b/src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js index 95151c7d..4f16ced8 100644 --- a/src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js +++ b/src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js @@ -4,11 +4,25 @@ */ export class GrupoEmpleadosLectura { - constructor({ idGrupo, nombre, descripcion, setsProductos = [], empleados = [] }) { + constructor({ + idGrupo, + nombre, + descripcion, + setsProductos = [], + idsSetProductos = [], + empleados = [], + idsEmpleados = [], + empleadosActualizar = [], + setProductosActualizar = [], + }) { this.idGrupo = idGrupo; this.nombre = nombre; this.descripcion = descripcion; this.setsProductos = setsProductos; + this.idsSetProductos = idsSetProductos; this.empleados = empleados; + this.idsEmpleados = idsEmpleados; + this.empleadosActualizar = empleadosActualizar; + this.setProductosActualizar = setProductosActualizar; } } diff --git a/src/Dominio/Modelos/Eventos/Eventos.js b/src/Dominio/Modelos/Eventos/Eventos.js index 49527eb7..748c96ae 100644 --- a/src/Dominio/Modelos/Eventos/Eventos.js +++ b/src/Dominio/Modelos/Eventos/Eventos.js @@ -1,9 +1,26 @@ export class Evento { - constructor({ idEvento, nombre, descripcion, puntos, periodoRenovacion, renovacion }) { + + /** + * Clase que representa un evento. + * + * @constructor + * @param {Object} params - Parámetros del evento. + * @param {int} params.idEvento - ID del evento. + * @param {int} params.idCliente - ID del cliente asociado al evento. + * @param {string} params.nombre - Nombre del evento. + * @param {string} params.descripcion - Descripción del evento. + * @param {float} params.puntos - Puntos asociados al evento. + * @param {float} params.multiplicador - Multiplicador de puntos del evento. + * @param {string} params.periodoRenovacion - Periodo de renovación del evento. + * @param {bool} params.renovacion - Indica si el evento es renovable. + */ + constructor({ idEvento = 0, idCliente = 0, nombre = '', descripcion = '', puntos = 0, multiplicador = 0, periodoRenovacion = '', renovacion = false }) { this.idEvento = idEvento; + this.idCliente = idCliente; this.nombre = nombre; this.descripcion = descripcion; this.puntos = puntos; + this.multiplicador = multiplicador; this.periodo = periodoRenovacion; this.renovacion = renovacion; } diff --git a/src/Dominio/Modelos/Pedidos/PedidoModelo.js b/src/Dominio/Modelos/Pedidos/PedidoModelo.js new file mode 100644 index 00000000..8a19a7ca --- /dev/null +++ b/src/Dominio/Modelos/Pedidos/PedidoModelo.js @@ -0,0 +1,9 @@ +export class PedidoModelo { + constructor({ idPedido, estado, precioTotal, idEnvio, idPago }) { + this.idPedido = idPedido || ''; + this.estado = estado || ''; + this.precioTotal = precioTotal || 0; + this.idEnvio = idEnvio || ''; + this.idPago = idPago || ''; + } +} diff --git a/src/Dominio/Modelos/Productos/InfoProducto.js b/src/Dominio/Modelos/Productos/InfoProducto.js new file mode 100644 index 00000000..d9ef51e9 --- /dev/null +++ b/src/Dominio/Modelos/Productos/InfoProducto.js @@ -0,0 +1,61 @@ +/* + * Modelo de información de un producto + * RF[28] Lee información del producto - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF28 + */ + +export class InfoProducto { + constructor({ producto = {} }) { + this.idProducto = producto.idProducto ?? null; + this.nombreComun = producto.nombreComun ?? ''; + this.nombreComercial = producto.nombreComercial ?? ''; + this.tipoProducto = producto.tipoProducto ?? ''; + this.marca = producto.marca ?? ''; + this.modelo = producto.modelo ?? ''; + this.estado = producto.estado ?? null; + this.idProveedor = producto.idProveedor ?? null; + this.nombreProveedor = producto.nombreProveedor ?? ''; + this.precioVenta = producto.precioVenta ?? 0; + this.precioCliente = producto.precioCliente ?? 0; + this.precioPuntos = producto.precioPuntos ?? 0; + this.costo = producto.costo ?? 0; + this.envio = producto.envio ?? 0; + this.impuesto = producto.impuesto ?? 0; + this.descuento = producto.descuento ?? 0; + + // Variantes del producto (puede ser un arreglo vacío) + this.variantes = Array.isArray(producto.variantes) + ? producto.variantes.map((variante) => new VarianteProducto(variante)) + : []; + } +} + +class VarianteProducto { + constructor({ idVariante, nombreVariante, descripcion, opciones = [] }) { + this.idVariante = idVariante ?? null; + this.nombreVariante = nombreVariante ?? ''; + this.descripcion = descripcion ?? ''; + this.opciones = Array.isArray(opciones) + ? opciones.map((opcion) => new OpcionVariante(opcion)) + : []; + } +} + +class OpcionVariante { + constructor({ + estado, + cantidad, + descuento, + valorOpcion, + SKUcomercial, + SKUautomatico, + costoAdicional, + }) { + this.estado = estado ?? null; + this.cantidad = cantidad ?? 0; + this.descuento = descuento ?? 0; + this.valorOpcion = valorOpcion ?? ''; + this.SKUcomercial = SKUcomercial ?? ''; + this.SKUautomatico = SKUautomatico ?? ''; + this.costoAdicional = costoAdicional ?? 0; + } +} diff --git a/src/Dominio/Modelos/Roles/DetalleRol.js b/src/Dominio/Modelos/Roles/DetalleRol.js new file mode 100644 index 00000000..f9d7c9cc --- /dev/null +++ b/src/Dominio/Modelos/Roles/DetalleRol.js @@ -0,0 +1,23 @@ +/** + * @class DetalleRol + * Representa el detalle completo de un rol en el sistema. + */ +export class DetalleRol { + constructor({ idRol, nombre, descripcion, totalUsuarios, permisos }) { + this.idRol = idRol ?? null; + this.nombre = nombre ?? ''; + this.descripcion = descripcion ?? ''; + this.totalUsuarios = totalUsuarios ?? 0; + this.permisos = permisos ?? []; + } +} + +/** + * Convierte una respuesta JSON del backend en una instancia de DetalleRol. + * @param {object} respuestaJson + * @returns {DetalleRol} + */ +export function modeloDetalleRol(respuestaJson) { + const { rol } = respuestaJson; + return new DetalleRol(rol); +} \ No newline at end of file diff --git a/src/Dominio/Modelos/SetsProductos/CrearSetsProductos.js b/src/Dominio/Modelos/SetsProductos/CrearSetsProductos.js new file mode 100644 index 00000000..db3bd0f0 --- /dev/null +++ b/src/Dominio/Modelos/SetsProductos/CrearSetsProductos.js @@ -0,0 +1,13 @@ +/** + * Modelo para crear un set de productos + * Representa los datos necesarios para crear un nuevo set de productos. + */ +export class CrearSetsProducto { + constructor({ nombre, nombreVisible, descripcion, productos }) { + this.nombre = nombre; + this.nombreVisible = nombreVisible; + this.descripcion = descripcion; + this.activo = 1; + this.idProductos = productos.map(producto => producto.idProducto); + } +} \ No newline at end of file diff --git a/src/Dominio/Modelos/SetsProductos/SetProductos.js b/src/Dominio/Modelos/SetsProductos/SetProductos.js index b36cd24a..5335ac36 100644 --- a/src/Dominio/Modelos/SetsProductos/SetProductos.js +++ b/src/Dominio/Modelos/SetsProductos/SetProductos.js @@ -7,12 +7,13 @@ */ export class SetProductos { - constructor({ idSetProducto, nombre, descripcion, activo, productos, grupos }) { + constructor({ idSetProducto, nombre, descripcion, activo, productos, grupos, idsProductos }) { this.idSetProducto = idSetProducto; this.nombre = nombre; this.descripcion = descripcion; this.activo = activo; this.productos = productos; this.grupos = grupos; + this.idsProductos = idsProductos; } } diff --git a/src/Dominio/Modelos/Usuarios/ActualizarUsuario.js b/src/Dominio/Modelos/Usuarios/ActualizarUsuario.js new file mode 100644 index 00000000..76975f07 --- /dev/null +++ b/src/Dominio/Modelos/Usuarios/ActualizarUsuario.js @@ -0,0 +1,103 @@ +// Modelo para actualizar un usuario + +export const validarDatosActualizarUsuario = (datos, usuariosExistentes = []) => { + const errores = {}; + const emailValido = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + const telefonoValido = /^\d{10}$/; + const tieneCaracterEspecial = /[!@#$%^&*(),.?":{}|<>]/; + const tieneMayuscula = /[A-Z]/; + + if (!datos.idUsuario) { + errores.idUsuario = true; + } else if (datos.idUsuario.toString().length > 10) { + errores.idUsuario = 'El idUsuario no debe tener más de 10 caracteres'; + } + + if (!datos.nombreCompleto || datos.nombreCompleto.trim().length === 0) { + errores.nombreCompleto = 'El nombre completo es obligatorio'; + } + + if (!datos.correoElectronico) { + errores.correoElectronico = 'El correo electrónico es obligatorio'; + } else if (!emailValido.test(datos.correoElectronico)) { + errores.correoElectronico = 'Correo electrónico inválido'; + } else if ( + usuariosExistentes.some( + (usuario) => + usuario.correoElectronico === datos.correoElectronico && + usuario.idUsuario !== datos.idUsuario + ) + ) { + errores.correoElectronico = 'Este correo ya está registrado'; + } + + if (datos.contrasenia && /\s/.test(datos.contrasenia)) { + errores.contrasenia = 'La contraseña no debe contener espacios en blanco'; + } else if (datos.contrasenia && datos.contrasenia.length < 8) { + errores.contrasenia = 'La contraseña debe tener al menos 8 caracteres'; + } else if (datos.contrasenia && !tieneCaracterEspecial.test(datos.contrasenia)) { + errores.contrasenia = + 'Debe contener al menos uno de estos caracteres: ! @ # $ % ^ & * ( ) , . ? " : { } | < >'; + } else if (datos.contrasenia && !tieneMayuscula.test(datos.contrasenia)) { + errores.contrasenia = 'Debe contener al menos una letra mayúscula'; + } + + if (datos.contrasenia && !datos.confirmarContrasenia) { + errores.confirmarContrasenia = true; + } else if (datos.contrasenia && datos.contrasenia !== datos.confirmarContrasenia) { + errores.confirmarContrasenia = 'Las contraseñas no coinciden'; + } + + if (!datos.numeroTelefono) { + errores.numeroTelefono = 'El número de teléfono es obligatorio'; + } else if (!telefonoValido.test(datos.numeroTelefono)) { + errores.numeroTelefono = 'El número de teléfono debe tener exactamente 10 dígitos'; + } + + if (!datos.direccion || datos.direccion.trim().length === 0) { + errores.direccion = 'La dirección es obligatoria'; + } else if (datos.direccion.trim().length < 3) { + errores.direccion = 'La dirección debe tener al menos 3 caracteres'; + } else if (datos.direccion.length > 100) { + errores.direccion = 'La dirección no debe exceder 100 caracteres'; + } + + /*if (!datos.fechaNacimiento) { + errores.fechaNacimiento = 'La fecha de nacimiento es obligatoria'; + } else { + const hoy = new Date(); + const fecha = new Date(datos.fechaNacimiento); + + if (fecha > hoy) { + errores.fechaNacimiento = 'La fecha no puede ser futura'; + } + }*/ + + if (!datos.fechaNacimiento) { + errores.fechaNacimiento = true; + } else { + const hoy = new Date(); + const limiteInferiorFecha = new Date('1900-01-01'); + const fecha = new Date(datos.fechaNacimiento); + + if (fecha > hoy) { + errores.fechaNacimiento = 'La fecha no puede ser futura'; + } else if (fecha < limiteInferiorFecha) { + errores.fechaNacimiento = 'Ingresa una fecha válida'; + } + } + + if (!datos.genero) { + errores.genero = 'El género es obligatorio'; + } + + if (typeof datos.estatus === 'undefined' || datos.estatus === null) { + errores.estatus = 'El estatus es obligatorio'; + } + + if (!datos.idRol) { + errores.idRol = 'El rol es obligatorio'; + } + + return errores; +}; diff --git a/src/Dominio/Modelos/Usuarios/modeloCrearUsuario.js b/src/Dominio/Modelos/Usuarios/modeloCrearUsuario.js index ee46b317..b2e70466 100644 --- a/src/Dominio/Modelos/Usuarios/modeloCrearUsuario.js +++ b/src/Dominio/Modelos/Usuarios/modeloCrearUsuario.js @@ -27,11 +27,7 @@ export const validarDatosCrearUsuario = (datos, usuariosExistentes = []) => { errores.numeroTelefono = true; } else if (!telefonoValido.test(datos.numeroTelefono)) { errores.numeroTelefono = 'El número de teléfono debe tener exactamente 10 dígitos'; - } else if ( - usuariosExistentes.some( - (usuario) => usuario.telefono === datos.numeroTelefono - ) - ) { + } else if (usuariosExistentes.some((usuario) => usuario.telefono === datos.numeroTelefono)) { errores.numeroTelefono = 'Este número ya está registrado'; } if (!datos.direccion || datos.direccion.trim() === '') { @@ -58,20 +54,20 @@ export const validarDatosCrearUsuario = (datos, usuariosExistentes = []) => { errores.contrasenia = true; } else { const contraseniaSinEspacios = datos.contrasenia.replace(/\s/g, ''); // elimina todos los espacios - + if (datos.contrasenia.length < 8) { errores.contrasenia = 'La contraseña debe tener al menos 8 caracteres'; } else if (!tieneCaracterEspecial.test(datos.contrasenia)) { - errores.contrasenia + errores.contrasenia = 'Debe contener al menos uno de estos caracteres: ! @ # $ % ^ & * ( ) , . ? " : { } | < >'; } else if (contraseniaSinEspacios.length < 2) { - errores.contrasenia + errores.contrasenia = 'La contraseña no puede estar compuesta solo de espacios y un carácter especial'; - } else if (!tieneMayuscula.test(datos.contrasenia)) { + } else if (!tieneMayuscula.test(datos.contrasenia)) { errores.contrasenia = 'Debe contener al menos una letra mayúscula'; } } - + if (!datos.confirmarContrasenia) { errores.confirmarContrasenia = true; } else if (datos.contrasenia !== datos.confirmarContrasenia) { diff --git a/src/Dominio/Repositorios/Categorias/repositorioActualizarCategoria.js b/src/Dominio/Repositorios/Categorias/repositorioActualizarCategoria.js new file mode 100644 index 00000000..ff71f66b --- /dev/null +++ b/src/Dominio/Repositorios/Categorias/repositorioActualizarCategoria.js @@ -0,0 +1,40 @@ +// RF49 - Actualizar Categoría - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF49] +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +/** + * Repositorio para actualizar una categoría en el sistema. + * + * RF49 - Actualizar Categoría + * @see https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF49 + */ +export class RepositorioActualizarCategoria { + /** + * Envía una solicitud PUT al backend para actualizar una categoría. + * + * @param {number} idCategoria - ID de la categoría a actualizar. + * @param {object} datosCategoria - Datos nuevos de la categoría. + * @returns {Promise} - Respuesta del servidor. + * @throws {Error} - Si ocurre un error durante la petición. + */ + static async actualizar(idCategoria, datosCategoria) { + try { + const respuesta = await axios.put( + `${RUTAS_API.CATEGORIAS.ACTUALIZAR}/${idCategoria}`, + datosCategoria, + { + withCredentials: true, + headers: { + 'x-api-key': API_KEY, + }, + } + ); + return respuesta.data; + } catch (error) { + const mensaje = error?.response?.data?.mensaje || 'Error al actualizar la categoría'; + throw new Error(mensaje); + } + } +} \ No newline at end of file diff --git a/src/Dominio/Repositorios/Cuotas/repositorioActualizarCuota.js b/src/Dominio/Repositorios/Cuotas/repositorioActualizarCuota.js new file mode 100644 index 00000000..44152a2c --- /dev/null +++ b/src/Dominio/Repositorios/Cuotas/repositorioActualizarCuota.js @@ -0,0 +1,23 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioActualizarCuota { + static async actualizarSetCuotas(datosActualizacion) { + try { + const respuesta = await axios.put( + RUTAS_API.CUOTAS.ACTUALIZAR_SET_CUOTAS, + datosActualizacion, + { + headers: { 'x-api-key': API_KEY }, + withCredentials: true, + } + ); + return respuesta.data; + } catch (error) { + const mensaje = error.response?.data?.mensaje || 'Error al actualizar el set de cuotas.'; + throw new Error(mensaje); + } + } +} \ No newline at end of file diff --git a/src/Dominio/Repositorios/Cuotas/repositorioLeerCuota.js b/src/Dominio/Repositorios/Cuotas/repositorioLeerCuota.js new file mode 100644 index 00000000..dd5e9121 --- /dev/null +++ b/src/Dominio/Repositorios/Cuotas/repositorioLeerCuota.js @@ -0,0 +1,38 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; +import { LeerCuota } from '@Modelos/Cuotas/LeerCuota'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioLeerCuota { + /** + * Consulta los datos de una cuota específica por ID + * @param {number} idCuota - ID de la cuota a consultar + * @returns {Promise<{cuota: LeerCuota, mensaje: string}>} + * + * @see [RF[38] Leer cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF38)] + */ + static async obtenerPorId(idSetCuota) { + try { + const respuesta = await axios.post( + RUTAS_API.CUOTAS.LEER_SET_CUOTAS, + { idSetCuota }, + { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + } + ); + const { setCuota, mensaje } = respuesta.data; + + return { + cuota: setCuota, + mensaje, + }; + } catch (error) { + const mensaje = error.response?.data?.mensaje || 'Error al obtener datos de la cuota.'; + throw new Error(mensaje); + } + } +} diff --git a/src/Dominio/Repositorios/Cuotas/repositorioObtenerOpcionesCuotas.js b/src/Dominio/Repositorios/Cuotas/repositorioObtenerOpcionesCuotas.js new file mode 100644 index 00000000..bd196510 --- /dev/null +++ b/src/Dominio/Repositorios/Cuotas/repositorioObtenerOpcionesCuotas.js @@ -0,0 +1,25 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioObtenerOpcionesCuotas { + static async obtenerOpciones(idCliente) { + try { + + const respuesta = await axios.get( + `${RUTAS_API.CUOTAS.OBTENER_OPCIONES}?idCliente=${idCliente}`, + { + headers: { 'x-api-key': API_KEY }, + withCredentials: true, + } + ); + + return respuesta.data; + } catch (error) { + console.error('Error en repositorio opciones:', error); + const mensaje = error.response?.data?.mensaje || 'Error al obtener opciones de productos.'; + throw new Error(mensaje); + } + } +} \ No newline at end of file diff --git a/src/Dominio/Repositorios/Empleados/RepositorioActualizarGrupoEmpleados.js b/src/Dominio/Repositorios/Empleados/RepositorioActualizarGrupoEmpleados.js new file mode 100644 index 00000000..1f9c52e7 --- /dev/null +++ b/src/Dominio/Repositorios/Empleados/RepositorioActualizarGrupoEmpleados.js @@ -0,0 +1,45 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioActualizarGrupoEmpleados { + /** + * * Actualiza los datos de un grupo de empleados específico + * @param {number} idGrupo - ID del grupo de empleados a actualizar + * @param {string} nombre - Nuevo nombre del grupo de empleados + * @param {string} descripcion - Nueva descripción del grupo de empleados + * @param {array} empleados - Lista de IDs de empleados a agregar al grupo + * @param {array} setsDeProductos - Lista de IDs de sets de productos a agregar al grupo + * @returns {Promise<{mensaje: string}>} + * + * @see [RF[24] Actualizar grupo de empleados - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF24) + */ + static async actualizarGrupoEmpleados(idGrupo, nombre, descripcion, empleados, setsDeProductos) { + try { + const respuesta = await axios.put( + `${RUTAS_API.EMPLEADOS.ACTUALIZAR_GRUPO}`, + { + idGrupoEmpleado: idGrupo, + nombre, + descripcion, + empleados, + setsDeProductos, + }, + { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + } + ); + + return respuesta; + } catch (error) { + if (error.response?.data?.mensaje) { + throw new Error(error.response.data.mensaje); + } + throw error; + } + } +} diff --git a/src/Dominio/Repositorios/Empleados/RepositorioCrearEmpleado.js b/src/Dominio/Repositorios/Empleados/RepositorioCrearEmpleado.js index 9e001690..dbd88182 100644 --- a/src/Dominio/Repositorios/Empleados/RepositorioCrearEmpleado.js +++ b/src/Dominio/Repositorios/Empleados/RepositorioCrearEmpleado.js @@ -1,27 +1,27 @@ //RF[16] Crea Empleado - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF16] -import axios from 'axios'; import { RUTAS_API } from '@Constantes/rutasAPI'; +import axios from 'axios'; const API_KEY = import.meta.env.VITE_API_KEY; +/** + * Crea un nuevo empleado + * @param {Object} datosEmpleado - Los datos del empleado a crear + * @returns {Promise} - Promise con la respuesta del servidor + */ export class RepositorioCrearEmpleado { - static async crear(datos) { + static async crear(cambios) { try { - const respuesta = await axios.post( - RUTAS_API.EMPLEADOS.CREAR_EMPLEADO, - { datos }, - { - withCredentials: true, - headers: { - 'x-api-key': API_KEY, - }, - } - ); + const respuesta = await axios.post(RUTAS_API.EMPLEADOS.CREAR, cambios, { + withCredentials: true, + headers: { + 'x-api-key': API_KEY, + }, + }); return respuesta.data; } catch (error) { - const mensaje = error?.response?.data?.mensaje || 'Error al crear'; - throw new Error(mensaje); + throw new Error(error?.response?.data?.mensaje || 'Error al crear empleado'); } } } diff --git a/src/Dominio/Repositorios/Empleados/RepositorioExportarEmpleados.js b/src/Dominio/Repositorios/Empleados/RepositorioExportarEmpleados.js new file mode 100644 index 00000000..b6b31fa9 --- /dev/null +++ b/src/Dominio/Repositorios/Empleados/RepositorioExportarEmpleados.js @@ -0,0 +1,32 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +/** + * Solicita la exportación de empleados seleccionados al backend y retorna el contenido CSV como string. + * + * @param {number[]} idsEmpleado - Arreglo de IDs de empleados seleccionados para exportar. + * @returns {Promise<{ mensaje: string, csv: string }>} Respuesta con mensaje y contenido CSV. + * @throws {Error} Si la petición falla o el servidor devuelve un mensaje de error. + * + * @see [RF59 - Exportar Empleados](https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF59) + */ + +export const exportarEmpleados = async (idsEmpleado) => { + try { + const response = await axios.post( + RUTAS_API.EMPLEADOS.EXPORTAR_EMPLEADOS, + { idsEmpleado }, + { + withCredentials: true, + headers: { 'x-api-key': API_KEY }, + } + ); + return response.data; + } catch (error) { + throw new Error( + error.response?.data?.mensaje || 'Error al importar empleados en el servidor' + ); + } +}; \ No newline at end of file diff --git a/src/Dominio/Repositorios/Eventos/RepositorioCrearEvento.js b/src/Dominio/Repositorios/Eventos/RepositorioCrearEvento.js new file mode 100644 index 00000000..5b4970ce --- /dev/null +++ b/src/Dominio/Repositorios/Eventos/RepositorioCrearEvento.js @@ -0,0 +1,41 @@ +// RF36 - Crear Evento - [https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF36] + +import axios from 'axios'; +import { RUTAS_API } from '@Utilidades/Constantes/rutasAPI'; +import { Evento } from '@Dominio/Modelos/Eventos/Eventos'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioCrearEvento { + /** + * Crea un nuevo evento en la API + * @param {Evento} evento - El evento a crear + * @returns {Promise<{evento: Evento, mensaje: string}>} + */ + static async crearEvento(evento) { + + const body = { + idCliente: evento.idCliente || "", + nombre: evento.nombre || "", + descripcion: evento.descripcion || "", + puntos: evento.puntos || 0, + multiplicador: evento.multiplicador || 1, + periodoRenovacion: evento.periodo || "", + renovacion: evento.renovacion || false, + } + + + try { + const respuesta = await axios.post(RUTAS_API.EVENTOS.CREAR_EVENTO, body, { + withCredentials: true, + headers: { + 'x-api-key': API_KEY, + }, + }); + return new Evento(respuesta.data); + } catch (error) { + const mensaje = error?.response?.data?.mensaje || 'Error al crear el evento'; + throw new Error(mensaje); + } + } +} diff --git a/src/Dominio/Repositorios/Pedidos/repositorioActualizarPedido.js b/src/Dominio/Repositorios/Pedidos/repositorioActualizarPedido.js new file mode 100644 index 00000000..822625d7 --- /dev/null +++ b/src/Dominio/Repositorios/Pedidos/repositorioActualizarPedido.js @@ -0,0 +1,44 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioActualizarPedido { + /** + * * Actualiza los datos de un pedido específico + * @param {Object} pedido - Objeto que contiene los datos del pedido a actualizar + * @returns {Promise<{mensaje: string}>} + * + * @see [RF[62] Actualizar pedido - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF62)] + */ + static async actualizarPedido(pedido) { + try { + const respuesta = await axios.put( + `${RUTAS_API.PEDIDOS.ACTUALIZAR_PEDIDO}`, + { + idPedido: pedido.idPedido, + estado: pedido.estado, + precioTotal: pedido.precioTotal, + idEnvio: pedido.idEnvio, + idPago: pedido.idPago, + }, + { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + } + ); + return respuesta; + } catch (error) { + console.error('[Error RepositorioActualizarPedido]:', error.response?.data || error.message); + + // Lanzar el error con el mensaje del backend si existe + if (error.response?.data?.mensaje) { + throw new Error(error.response.data.mensaje); + } + // Si no hay mensaje del backend, lanzar el error original + throw error; + } + } +} diff --git a/src/Dominio/Repositorios/Productos/RepositorioExportarProducto.js b/src/Dominio/Repositorios/Productos/RepositorioExportarProducto.js new file mode 100644 index 00000000..da092c63 --- /dev/null +++ b/src/Dominio/Repositorios/Productos/RepositorioExportarProducto.js @@ -0,0 +1,29 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +/** + * Solicita la exportación de productos seleccionados al backend y retorna el contenido. + * + * @param {number[]} idsProducto - Arreglo de IDs de productos seleccionados para exportar. + * @returns {Promise} Contenido binario del archivo. + * @throws {Error} Si la petición falla o el servidor devuelve un mensaje de error. + * @see [RF58 - Exportar Productos](https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF58) + */ +export const exportarProductos = async (idsProducto) => { + try { + const respuesta = await axios.post( + RUTAS_API.PRODUCTOS.EXPORTAR_PRODUCTOS, + { idsProducto }, + { + withCredentials: true, + responseType: 'blob', + headers: { 'x-api-key': API_KEY }, + } + ); + return respuesta.data; + } catch (error) { + throw new Error(error.respuesta?.data?.mensaje || 'Error al exportar productos en el servidor'); + } +}; diff --git a/src/Dominio/Repositorios/Productos/RepositorioImportarProductos.js b/src/Dominio/Repositorios/Productos/RepositorioImportarProductos.js new file mode 100644 index 00000000..bfd48d05 --- /dev/null +++ b/src/Dominio/Repositorios/Productos/RepositorioImportarProductos.js @@ -0,0 +1,39 @@ +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 RepositorioImportarProductos { + /** + * Recibe un arreglo de objetos `{ productoRaw, variantesRaw }`, + * los convierte en instancias de ProductoCompleto y Variante, + * y los envía al backend como JSON para importación masiva. + * + * @param {Array<{ productoRaw: Object, variantesRaw: Array }>} productosParseados + * @returns {Promise} Respuesta del backend + */ + static async importarProductos(productosParseados) { + const productosConvertidos = productosParseados.map(({ productoRaw, variantesRaw }) => ({ + producto: new ProductoCompleto(productoRaw), + variantes: variantesRaw.map((va) => new Variante(va)), + })); + + try { + + const respuesta = await axios.post(RUTAS_API.PRODUCTOS.IMPORTAR, productosConvertidos, { + withCredentials: true, + headers: { + 'x-api-key': API_KEY, + 'Content-Type': 'application/json', + }, + }); + + return respuesta.data; + } catch (error) { + const mensaje = error?.response?.data?.mensaje || 'Error al importar productos desde CSV'; + throw new Error(mensaje); + } + } +} diff --git a/src/Dominio/Repositorios/Productos/RepositorioLeerProducto.js b/src/Dominio/Repositorios/Productos/RepositorioLeerProducto.js new file mode 100644 index 00000000..708c2fce --- /dev/null +++ b/src/Dominio/Repositorios/Productos/RepositorioLeerProducto.js @@ -0,0 +1,32 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI.js'; +import { InfoProducto } from '@Modelos/Productos/InfoProducto.js'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioLeerProducto { + static async obtenerPorId(idProducto) { + try { + const respuesta = await axios.get(RUTAS_API.PRODUCTOS.LEER_PRODCUTO, { + headers: { + 'x-api-key': API_KEY + }, + withCredentials: true, + params: { + idProducto + } + }); + + const { infoProducto } = respuesta.data; + + // Asegurarse de que existe y tiene al menos un elemento + if (!infoProducto || !Array.isArray(infoProducto) || infoProducto.length === 0) { + throw new Error('No se encontró información del producto.'); + } + + return new InfoProducto(infoProducto[0]); + } catch (error) { + throw new Error(error.response?.data?.mensaje || 'Error al obtener datos del producto.'); + } + } +} diff --git a/src/Dominio/Repositorios/Roles/RepositorioActualizarRol.js b/src/Dominio/Repositorios/Roles/RepositorioActualizarRol.js new file mode 100644 index 00000000..47541a86 --- /dev/null +++ b/src/Dominio/Repositorios/Roles/RepositorioActualizarRol.js @@ -0,0 +1,28 @@ +//RF[8] Leer Rol - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF8 + +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioActualizarRol { + static async actualizar(idRol, datosRol) { + try { + const datosRolActualizacion = {datosRol, idRol} + const respuesta = await axios.put( + `${RUTAS_API.ROLES.ACTUALIZAR}`, + { datosRolActualizacion }, + { + withCredentials: true, + headers: { + 'x-api-key': API_KEY, + }, + } + ); + return respuesta.data; + } catch (error) { + const mensaje = error?.response?.data?.mensaje || 'Error al actualizar el rol'; + throw new Error(mensaje); + } + } +} \ No newline at end of file diff --git a/src/Dominio/Repositorios/Roles/RepositorioLeerRol.js b/src/Dominio/Repositorios/Roles/RepositorioLeerRol.js new file mode 100644 index 00000000..d162dc32 --- /dev/null +++ b/src/Dominio/Repositorios/Roles/RepositorioLeerRol.js @@ -0,0 +1,23 @@ +import axios from 'axios'; +import { modeloDetalleRol } from '@Modelos/Roles/DetalleRol'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioLeerRol { + static async obtenerDetalle(idRol) { + try { + const { data } = await axios.get(`${RUTAS_API.ROLES.LEER_ROL}?idRol=${idRol}`, { + withCredentials: true, + headers: { + 'x-api-key': API_KEY, + }, + }); + + return modeloDetalleRol(data); + } catch (error) { + const mensaje = error.response?.data?.mensaje || 'Error al consultar detalle del rol'; + throw new Error(mensaje); + } + } +} \ No newline at end of file diff --git a/src/Dominio/Repositorios/SetsProductos/RepositorioActualizarSetProductos.js b/src/Dominio/Repositorios/SetsProductos/RepositorioActualizarSetProductos.js new file mode 100644 index 00000000..98c82c13 --- /dev/null +++ b/src/Dominio/Repositorios/SetsProductos/RepositorioActualizarSetProductos.js @@ -0,0 +1,44 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioActualizarSetProductos { + /** + * Actualiza los datos de un set de productos específico + * @param {number} idSet - ID del set de productos a actualizar + * @param {string} nombre - Nuevo nombre del set + * @param {string} descripcion - Nueva descripción del set + * @param {array} productos - Lista de IDs de productos + * @returns {Promise<{mensaje: string}>} + * + * @see [RF44] Actualizar set de productos - https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF44 + */ + static async actualizarSetProductos(idSet, nombre, activo, descripcion, productos) { + try { + const respuesta = await axios.put( + `${RUTAS_API.SETS_PRODUCTOS.ACTUALIZAR_SETS_PRODUCTO}`, + { + idSetProducto: idSet, + nombre, + activo, + descripcion, + productos, + }, + { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + } + ); + + return respuesta; + } catch (error) { + if (error.response?.data?.mensaje) { + throw new Error(error.response.data.mensaje); + } + throw error; + } + } +} diff --git a/src/Dominio/Repositorios/SetsProductos/RepositorioCrearSetsProducto.js b/src/Dominio/Repositorios/SetsProductos/RepositorioCrearSetsProducto.js new file mode 100644 index 00000000..b1fb8477 --- /dev/null +++ b/src/Dominio/Repositorios/SetsProductos/RepositorioCrearSetsProducto.js @@ -0,0 +1,24 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioCrearSetsProducto { + static async crearSetsProducto(nuevoSetsProductos) { + try{ + const respuesta = await axios.post( + RUTAS_API.SETS_PRODUCTOS.CREAR_SETS_PRODUCTOS, + {nuevoSetsProductos}, + { + headers: { + 'x-api-key': API_KEY + }, + withCredentials: true + } + ); + return respuesta + }catch(error){ + throw new Error(error.response?.data?.mensaje || 'Error de conexion, intentalo de nuevo mas tarde.') + } + } +} \ No newline at end of file diff --git a/src/Dominio/Repositorios/Usuarios/RepositorioActualizarUsuario.js b/src/Dominio/Repositorios/Usuarios/RepositorioActualizarUsuario.js new file mode 100644 index 00000000..cfc3ef8c --- /dev/null +++ b/src/Dominio/Repositorios/Usuarios/RepositorioActualizarUsuario.js @@ -0,0 +1,25 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioActualizarUsuario { + static async actualizar(cambios) { + try { + const respuesta = await axios.put( + RUTAS_API.USUARIOS.ACTUALIZAR_USUARIO, + { cambios }, + { + withCredentials: true, + headers: { + 'x-api-key': API_KEY, + }, + } + ); + return respuesta.data; + } catch (error) { + const mensaje = error?.response?.data?.mensaje || 'Error al actualizar'; + throw new Error(mensaje); + } + } +} diff --git a/src/Dominio/Servicios/obtenerEmpleados.js b/src/Dominio/Servicios/obtenerEmpleados.js new file mode 100644 index 00000000..df4b5803 --- /dev/null +++ b/src/Dominio/Servicios/obtenerEmpleados.js @@ -0,0 +1,29 @@ +import axios from 'axios'; + +const API_URL = import.meta.env.VITE_API_URL; +const API_KEY = import.meta.env.VITE_API_KEY; + +const obtenerEmpleados = async (clienteSeleccionado) => { + try { + const respuesta = await axios.post( + `${API_URL}/api/empleados/consultar-lista`, + { clienteSeleccionado }, + { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + } + ); + const filasFormateadas = respuesta.data.empleados.map((empleado) => ({ + id: empleado.idEmpleado, + correo: empleado.correoElectronico, + nombre: empleado.nombreCompleto, + area: empleado.areaTrabajo, + })); + return filasFormateadas; + } catch { + return []; + } +}; +export default obtenerEmpleados; diff --git a/src/Dominio/Servicios/obtenerGruposEmpleados.js b/src/Dominio/Servicios/obtenerGruposEmpleados.js new file mode 100644 index 00000000..66674e8b --- /dev/null +++ b/src/Dominio/Servicios/obtenerGruposEmpleados.js @@ -0,0 +1,38 @@ +import axios from 'axios'; + +const API_URL = import.meta.env.VITE_API_URL; +const API_KEY = import.meta.env.VITE_API_KEY; + +/** + * Servicio para consultar la lista de grupos de empleados. + * Basado en RF22: Consulta Lista de Grupo Empleados + * https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF22 + */ +const obtenerGruposEmpleados = async (clienteSeleccionado) => { + try { + const respuesta = await axios.post( + `${API_URL}/api/empleados/consultar-grupos`, + { clienteSeleccionado }, + { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + } + ); + + const gruposFormateados = respuesta.data.grupos.map((grupo) => ({ + id: grupo.idGrupo, + nombre: grupo.geNombre, + descripcion: grupo.descripcion, + totalEmpleados: grupo.totalEmpleados, + })); + + return gruposFormateados; + } catch (error) { + console.error('Error al obtener grupos de empleados:', error); + return []; + } +}; + +export default obtenerGruposEmpleados; diff --git a/src/Dominio/Servicios/obtenerProductosCategoria.js b/src/Dominio/Servicios/obtenerProductosCategoria.js new file mode 100644 index 00000000..4d2fcbf5 --- /dev/null +++ b/src/Dominio/Servicios/obtenerProductosCategoria.js @@ -0,0 +1,38 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +/** + * Servicio para obtener la lista de productos disponibles para categorías. + * + * RF49 - Actualizar Categoría + * @returns {Promise>} Lista formateada de productos. + */ +const obtenerProductosCategoria = async () => { + try { + const respuesta = await axios.post( + RUTAS_API.PRODUCTOS.CONSULTAR_LISTA, + {}, + { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + } + ); + + const productosCrudos = respuesta?.data?.listaProductos || []; + + const productosFormateados = productosCrudos.map((producto) => ({ + id: producto.idProducto, + nombre: producto.nombreComun, + })); + + return productosFormateados; + } catch { + return []; + } +}; + +export default obtenerProductosCategoria; \ No newline at end of file diff --git a/src/Dominio/Servicios/obtenerSetsProductos.js b/src/Dominio/Servicios/obtenerSetsProductos.js new file mode 100644 index 00000000..18fdc4ab --- /dev/null +++ b/src/Dominio/Servicios/obtenerSetsProductos.js @@ -0,0 +1,31 @@ +import axios from 'axios'; + +const API_URL = import.meta.env.VITE_API_URL; +const API_KEY = import.meta.env.VITE_API_KEY; + +const obtenerSetsProductos = async (clienteSeleccionado) => { + try { + const respuesta = await axios.post( + `${API_URL}/api/sets-productos/consultar-lista`, + { clienteSeleccionado }, + { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + } + ); + + const filasFormateadas = respuesta.data.setsProductos.map((producto) => ({ + id: producto.idSetProducto, + nombreProducto: producto.nombre, + activo: producto.activo, + })); + + return filasFormateadas; + } catch { + return []; + } +}; + +export default obtenerSetsProductos; diff --git a/src/Utilidades/Constantes/rutas.js b/src/Utilidades/Constantes/rutas.js index 9e391155..a4840613 100644 --- a/src/Utilidades/Constantes/rutas.js +++ b/src/Utilidades/Constantes/rutas.js @@ -30,7 +30,11 @@ export const RUTAS = { CONSULTAR_CATEGORIAS: `${BASE_PRODUCTOS}/consultar-categorias`, }, PEDIDOS: { BASE: `${BASE_PEDIDOS}`, CONSULTAR_PEDIDOS: `${BASE_PEDIDOS}/consultar-lista` }, - CUOTAS: { BASE: `${BASE_CUOTAS}`, EDITAR_CUOTAS: `${BASE_CUOTAS}/editar-cuotas` }, + CUOTAS: { + BASE: `${BASE_CUOTAS}`, + EDITAR_CUOTAS: `${BASE_CUOTAS}/editar-cuotas`, + LEER_CUOTAS: `${BASE_CUOTAS}/leer-set-cuotas`, + }, EVENTOS: { BASE: `${BASE_EVENTOS}`, @@ -42,6 +46,7 @@ export const RUTAS = { BASE: '/usuarios', CONSULTAR_ROLES: '/consultar-roles', CONFIRMAR_CREACION: '/confirmar-creacion', + ACTUALIZAR_USUARIO: '/actualizar-usuario', }, }, SISTEMA_TIENDA: { diff --git a/src/Utilidades/Constantes/rutasAPI.js b/src/Utilidades/Constantes/rutasAPI.js index ea8837c0..4bf171a6 100644 --- a/src/Utilidades/Constantes/rutasAPI.js +++ b/src/Utilidades/Constantes/rutasAPI.js @@ -1,3 +1,5 @@ +import { A } from 'storybook/internal/components'; + const BASE_URL = import.meta.env.VITE_API_URL; const BASE_USUARIOS = `${BASE_URL}/api/usuarios`; const BASE_CATEGORIAS = `${BASE_URL}/api/categorias`; @@ -13,13 +15,13 @@ const BASE_EVENTOS = `${BASE_URL}/api/eventos`; const BASE_PAGOS = `${BASE_URL}/api/pagos`; const BASE_AUTENTICACION = `${BASE_URL}/api/autenticacion`; - export const RUTAS_API = { USUARIOS: { BASE: BASE_USUARIOS, CONSULTAR_LISTA: `${BASE_USUARIOS}/consultar-lista-usuarios`, CONSULTAR_USUARIO: `${BASE_USUARIOS}/consultar-usuario`, ELIMINAR_USUARIOS: `${BASE_USUARIOS}/eliminar-usuarios`, + ACTUALIZAR_USUARIO: `${BASE_USUARIOS}/actualizar-usuario`, }, AUTENTICACION: { @@ -27,18 +29,23 @@ export const RUTAS_API = { ACTIVAR_2FA: `${BASE_AUTENTICACION}/activar-2fa`, VERIFICAR_2FA: `${BASE_AUTENTICACION}/verificar-2fa`, }, - + CATEGORIAS: { BASE: BASE_CATEGORIAS, CONSULTAR_LISTA: `${BASE_CATEGORIAS}/consultar-lista-categorias`, CREAR: `${BASE_CATEGORIAS}/crear-categoria`, ELIMINAR_CATEGORIA: `${BASE_CATEGORIAS}/eliminar`, + LEER: `${BASE_CATEGORIAS}/leer`, + ACTUALIZAR: `${BASE_CATEGORIAS}/actualizar`, }, PRODUCTOS: { BASE: BASE_PRODUCTOS, CONSULTAR_LISTA: `${BASE_PRODUCTOS}/consultar-lista`, CREAR: `${BASE_PRODUCTOS}/crear`, ELIMINAR_PRODUCTO: `${BASE_PRODUCTOS}/eliminar`, + IMPORTAR: `${BASE_PRODUCTOS}/importar`, + LEER_PRODCUTO: `${BASE_PRODUCTOS}/leer-producto`, + EXPORTAR_PRODUCTOS: `${BASE_PRODUCTOS}/exportar-productos` }, PROVEEDORES: { BASE: BASE_PROVEEDORES, @@ -49,6 +56,8 @@ export const RUTAS_API = { BASE: BASE_SETS_PRODUCTOS, CONSULTAR_LISTA: `${BASE_SETS_PRODUCTOS}/consultar-lista`, ELIMINAR_SET_PRODUCTOS: `${BASE_SETS_PRODUCTOS}/eliminar`, + CREAR_SETS_PRODUCTOS: `${BASE_SETS_PRODUCTOS}/crear`, + ACTUALIZAR_SETS_PRODUCTO: `${BASE_SETS_PRODUCTOS}/actualizar`, }, CLIENTES: { BASE: BASE_CLIENTES, @@ -61,6 +70,7 @@ export const RUTAS_API = { }, EMPLEADOS: { BASE: BASE_EMPLEADOS, + CREAR: `${BASE_EMPLEADOS}/crear`, CONSULTAR_LISTA: `${BASE_EMPLEADOS}/consultar-lista`, CONSULTAR_GRUPOS: `${BASE_EMPLEADOS}/consultar-grupo`, ELIMINAR_EMPLEADO: `${BASE_EMPLEADOS}/eliminar`, @@ -69,26 +79,35 @@ export const RUTAS_API = { LEER_GRUPO: `${BASE_EMPLEADOS}/leer-grupo`, CREAR_GRUPO: `${BASE_EMPLEADOS}/crear-grupo`, ACTUALIZAR_EMPLEADO: `${BASE_EMPLEADOS}/actualizar`, + EXPORTAR_EMPLEADOS: `${BASE_EMPLEADOS}/exportar-empleados`, + ACTUALIZAR_GRUPO: `${BASE_EMPLEADOS}/actualizar-grupo`, }, CUOTAS: { BASE: BASE_CUOTAS, CREAR_CUOTA: `${BASE_CUOTAS}/crear-cuota`, CONSULTAR_LISTA: `${BASE_CUOTAS}/consultar-lista`, ELIMINAR_SET_CUOTAS: `${BASE_CUOTAS}/eliminar-set-cuotas`, + LEER_SET_CUOTAS: `${BASE_CUOTAS}/leer-set-cuotas`, + ACTUALIZAR_SET_CUOTAS: `${BASE_CUOTAS}/actualizar-set-cuotas`, + OBTENER_OPCIONES: `${BASE_CUOTAS}/obtener-opciones`, }, ROLES: { BASE: BASE_ROLES, CONSULTAR_LISTA: `${BASE_ROLES}/consultar-lista`, CREAR_ROL: `${BASE_ROLES}/crear-rol`, ELIMINAR_ROL: `${BASE_ROLES}/eliminar`, + LEER_ROL: `${BASE_ROLES}/leer`, + ACTUALIZAR: `${BASE_ROLES}/actualizar-rol`, }, PEDIDOS: { BASE: BASE_PEDIDOS, CONSULTAR_LISTA: `${BASE_PEDIDOS}/consultar-lista`, ELIMINAR_PEDIDO: `${BASE_PEDIDOS}/eliminar`, + ACTUALIZAR_PEDIDO: `${BASE_PEDIDOS}/actualizar-pedido`, }, EVENTOS: { BASE: BASE_EVENTOS, + CREAR_EVENTO: `${BASE_EVENTOS}/crear`, CONSULTAR_LISTA: `${BASE_EVENTOS}/consultar-lista-eventos`, ELIMINAR_EVENTO: `${BASE_EVENTOS}/eliminar`, CONSULTAR_EVENTO: `${BASE_EVENTOS}/consultar-evento`, diff --git a/src/Vistas/Componentes/Atomos/Boton.jsx b/src/Vistas/Componentes/Atomos/Boton.jsx index 6a49b67e..40af2962 100644 --- a/src/Vistas/Componentes/Atomos/Boton.jsx +++ b/src/Vistas/Componentes/Atomos/Boton.jsx @@ -15,6 +15,7 @@ const Boton = ({ label, onClick, construccion = false, + deshabilitado = false, ...props }) => { const theme = useTheme(); @@ -64,6 +65,7 @@ const Boton = ({ }} onClick={onClick} construccion={estiloDeshabilitado} + disabled={deshabilitado} {...props} > {label} @@ -83,6 +85,7 @@ Boton.propTypes = { label: PropTypes.string.isRequired, onClick: PropTypes.func, construccion: PropTypes.bool, + deshabilitado: PropTypes.bool, }; export default Boton; diff --git a/src/Vistas/Componentes/Atomos/CampoSelect.jsx b/src/Vistas/Componentes/Atomos/CampoSelect.jsx index 8b693824..330b19be 100644 --- a/src/Vistas/Componentes/Atomos/CampoSelect.jsx +++ b/src/Vistas/Componentes/Atomos/CampoSelect.jsx @@ -18,6 +18,7 @@ const CampoSelect = ({ helperText = '', disabled = false, size = 'small', + margin = 'normal', ...props }) => { return ( @@ -27,6 +28,7 @@ const CampoSelect = ({ error={error} size={size} disabled={disabled} + margin={margin} > {label} + + {cargando ? ( + + ) : ( + + )} + + + {cargando + ? 'Cargando…' + : isDragActive + ? 'Suelta el archivo aquí…' + : 'Arrastra tu CSV o haz clic para seleccionarlo (5 MB máximo)'} + + + ); +}; + +export default ContenedorImportarProductos; diff --git a/src/Vistas/Componentes/Organismos/Cuotas/ModalEditarCuotas.jsx b/src/Vistas/Componentes/Organismos/Cuotas/ModalEditarCuotas.jsx new file mode 100644 index 00000000..b90e5630 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Cuotas/ModalEditarCuotas.jsx @@ -0,0 +1,359 @@ +import { useState, useEffect } from 'react'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import CampoTexto from '@Atomos/CampoTexto'; +import { NumeroInput } from '@Atomos/NumeroInput'; +import Switch from '@Atomos/Switch'; +import { useActualizarCuota } from '@Hooks/Cuotas/useActualizarCuota'; +import { useObtenerOpcionesCuotas } from '@Hooks/Cuotas/useObtenerOpcionesCuotas'; +import { + Box, + Typography, + IconButton, + Select, + MenuItem, + FormControl, + InputLabel, + TextField, + Alert, +} from '@mui/material'; +import { Delete as DeleteIcon, Add as AddIcon } from '@mui/icons-material'; + +const ModalEditarCuotas = ({ + open, + cuotaOriginal, + onClose, + onActualizado, + cargandoDetalle, + errorDetalle +}) => { + const { actualizarCuota, cargando, error } = useActualizarCuota(); + const { opciones, cargando: cargandoOpciones } = useObtenerOpcionesCuotas(); + + const [datos, setDatos] = useState({ + idCuotaSet: null, + nombre: '', + descripcion: '', + periodoRenovacion: 6, + renovacionHabilitada: false, + productos: [] + }); + + const [alerta, setAlerta] = useState(''); + + useEffect(() => { + if (cuotaOriginal && open) { + const productosFormateados = (cuotaOriginal.productos || []).map(productoOriginal => ({ + idProducto: productoOriginal.idProducto ? String(productoOriginal.idProducto) : '', + nombreProducto: productoOriginal.nombreProducto || productoOriginal.nombreComun || productoOriginal.nombre || '', + limite: productoOriginal.limite || productoOriginal.cuota_valor || 0, + limiteActual: productoOriginal.limiteActual || productoOriginal.limite_actual || 0 + })); + + setDatos({ + idCuotaSet: cuotaOriginal.idCuotaSet || cuotaOriginal.idSetCuota, + nombre: cuotaOriginal.nombre || '', + descripcion: cuotaOriginal.descripcion || '', + periodoRenovacion: cuotaOriginal.periodoRenovacion || 6, + renovacionHabilitada: cuotaOriginal.renovacionHabilitada === 1 || cuotaOriginal.renovacionHabilitada === true, + productos: productosFormateados + }); + setAlerta(''); + } + }, [cuotaOriginal, open]); + + useEffect(() => { + if (!open) { + setAlerta(''); + } + }, [open]); + + useEffect(() => { + if (alerta) { + const timer = setTimeout(() => { + setAlerta(''); + }, 3000); + return () => clearTimeout(timer); + } + }, [alerta]); + + const agregarProducto = () => { + setDatos(prev => ({ + ...prev, + productos: [...prev.productos, { idProducto: '', nombreProducto: '', limite: 0, limiteActual: 0 }] + })); + }; + + const eliminarProducto = (index) => { + setDatos(prev => ({ + ...prev, + productos: prev.productos.filter((productoIgnorado, idx) => idx !== index) + })); + }; + + const cambiarProducto = (index, campo, valor) => { + setDatos(prev => ({ + ...prev, + productos: prev.productos.map((producto, idxProducto) => { + if (idxProducto === index) { + const nuevoProducto = { ...producto, [campo]: valor }; + + if (campo === 'idProducto') { + const opcionSeleccionada = opciones?.find(op => op.id == valor); + nuevoProducto.nombreProducto = opcionSeleccionada?.nombreProducto || ''; + + const yaExiste = prev.productos.some((otroProducto, idx) => + idx !== index && otroProducto.idProducto === valor && valor !== ''); + + if (yaExiste && valor !== '') { + const nombreProducto = opcionSeleccionada?.nombreProducto || 'este producto'; + setAlerta(`${nombreProducto} ya está asignado a esta cuota`); + return producto; + } + } + + return nuevoProducto; + } + return producto; + }) + })); + }; + + const guardar = async () => { + setAlerta(''); + + if (!datos.nombre.trim()) { + setAlerta('El nombre de la cuota es obligatorio'); + return; + } + + if (datos.periodoRenovacion < 1 || datos.periodoRenovacion > 12) { + setAlerta('El período de renovación debe estar entre 1 y 12 meses'); + return; + } + + const productosValidos = datos.productos.filter(producto => producto.idProducto && producto.idProducto !== ''); + + try { + await actualizarCuota({ + idCuotaSet: datos.idCuotaSet, + cambios: { + nombre: datos.nombre.trim(), + descripcion: datos.descripcion.trim(), + periodoRenovacion: datos.periodoRenovacion, + renovacionHabilitada: datos.renovacionHabilitada, + productos: productosValidos.map(producto => ({ + idProducto: Number(producto.idProducto), + limite: Number(producto.limite) || 0, + limiteActual: Number(producto.limiteActual) || 0 + })) + } + }); + + onActualizado(); + onClose(); + } catch (error) { + let mensajeAmigable = 'Ocurrió un error al actualizar la cuota'; + + if (error.message.includes('Duplicate')) { + mensajeAmigable = 'No se puede guardar: hay productos duplicados'; + } else if (error.message.includes('foreign key')) { + mensajeAmigable = 'Error: producto no válido seleccionado'; + } else if (error.message.includes('Network')) { + mensajeAmigable = 'Sin conexión a internet'; + } + + setAlerta(mensajeAmigable); + } + }; + + if (cargandoDetalle) { + return ( + + + Cargando información del set de cuotas... + + + ); + } + + if (errorDetalle) { + return ( + + + Error al cargar la información: {errorDetalle} + + + ); + } + + return ( + + + Información Básica + + setDatos(prev => ({ ...prev, nombre: evento.target.value }))} + fullWidth + sx={{ mb: 2 }} + inputProps={{ maxLength: 50 }} + helperText={`${datos.nombre.length}/50`} + /> + + setDatos(prev => ({ ...prev, descripcion: evento.target.value }))} + fullWidth + multiline + rows={3} + sx={{ mb: 2 }} + inputProps={{ maxLength: 150 }} + helperText={`${datos.descripcion.length}/150`} + /> + + + setDatos(prev => ({ + ...prev, + periodoRenovacion: parseInt(evento.target.value) || 6 + }))} + min={1} + max={12} + sx={{ flex: 1 }} + /> + + setDatos(prev => ({ + ...prev, + renovacionHabilitada: evento.target.checked + }))} + /> + + + + Productos Asociados ({datos.productos.length}) + + + + {!cargandoOpciones && (!opciones || opciones.length === 0) && ( + + No se pudieron cargar las opciones de productos. Verifica la conexión con el servidor. + + )} + + {datos.productos.length === 0 ? ( + + No hay productos asociados. Haz clic en + para agregar uno. + + ) : ( + datos.productos.map((producto, index) => ( + + + + Producto {index + 1} + {producto.nombreProducto && ( + + {' '} - {producto.nombreProducto} + + )} + + eliminarProducto(index)}> + + + + + + + Producto * + + + + { + const valor = evento.target.value.replace(/\D/g, ''); + if (valor.length <= 9) { + cambiarProducto(index, 'limite', parseInt(valor || '0', 10)); + } + }} + inputProps={{ min: 0 }} + sx={{ minWidth: 120 }} + /> + + { + const valor = evento.target.value.replace(/\D/g, ''); + if (valor.length <= 9) { + cambiarProducto(index, 'limiteActual', parseInt(valor || '0', 10)); + } + }} + inputProps={{ min: 0 }} + sx={{ minWidth: 120 }} + /> + + + + )) + )} + + + {(alerta || error) && ( + + setAlerta('')}> + {alerta || 'Error del sistema: Por favor intenta de nuevo'} + + + )} + + ); +}; + +export default ModalEditarCuotas; diff --git a/src/Vistas/Componentes/Organismos/Eventos/ModalCrearEvento.jsx b/src/Vistas/Componentes/Organismos/Eventos/ModalCrearEvento.jsx new file mode 100644 index 00000000..acc6b2d6 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Eventos/ModalCrearEvento.jsx @@ -0,0 +1,142 @@ +// RF36 - Crear Evento - [https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF36] + +import { useState, useEffect } from 'react'; +import FormularioCrearEvento from '@Organismos/Formularios/FormularioCrearEvento'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import { Evento } from '@SRC/Dominio/Modelos/Eventos/Eventos'; +import { useAuth } from '@SRC/hooks/AuthProvider'; + +/** + * Modal para crear un nuevo set de cuotas. + * + * @param {boolean} abierto - Controla si el modal está abierto o cerrado + * @param {function} onCerrar - Función callback que se ejecuta al cerrar el modal + * @param {function} onCreado - Función callback que se ejecuta cuando se crea exitosamente el evento + */ +const ModalCrearEvento = ({ abierto = false, onCerrar, onCreado }) => { + const { usuario } = useAuth(); + const clienteSeleccionado = usuario.clienteSeleccionado; + + const [nombreEvento, setNombreEvento] = useState(''); + const [descripcionEvento, setDescripcionEvento] = useState(''); + const [puntosEvento, setPuntosEvento] = useState(''); + const [multiplicadorEvento, setMultiplicadorEvento] = useState(''); + const [mostrarAlerta, setMostrarAlerta] = useState(false); + + const [nombreError, setNombreError] = useState(false); + const [descripcionError, setDescripcionError] = useState(false); + const [puntosError, setPuntosError] = useState(false); + const [multiplicadorError, setMultiplicadorError] = useState(false); + + const resetearCampos = () => { + setNombreEvento(''); + setDescripcionEvento(''); + setPuntosEvento(''); + setMultiplicadorEvento(''); + setMostrarAlerta(false); + }; + + const limpiarErrores = () => { + setNombreError(false); + setDescripcionError(false); + setPuntosError(false); + setMultiplicadorError(false); + }; + + const validarCampos = () => { + let errores = false; + + if (!nombreEvento.trim()) { + setNombreError(true); + setMostrarAlerta(true); + errores = true; + } + + if (puntosEvento === '' || puntosEvento < 0) { + setPuntosError(true); + setMostrarAlerta(true); + errores = true; + } + + if (multiplicadorEvento === '' || multiplicadorEvento < 0) { + setMultiplicadorError(true); + setMostrarAlerta(true); + errores = true; + } + + return errores; + } + + // Limpiar los campos cuando se cierra el modal + useEffect(() => { + if (!abierto) { + resetearCampos(); + limpiarErrores(); + } + }, [abierto]); + + const handleConfirmar = () => { + // Limpiar errores + limpiarErrores(); + setMostrarAlerta(false); + + // Validar campos + if (validarCampos()) { + return; + } + + // Crear el evento con los datos limpios (sin espacios innecesarios) + const nuevoEvento = new Evento({ + idCliente: clienteSeleccionado, + nombre: nombreEvento.trim(), + descripcion: descripcionEvento.trim(), + puntos: parseFloat(puntosEvento), + multiplicador: parseFloat(multiplicadorEvento), + periodoRenovacion: "Mensual", + renovacion: false + }); + + // Notificar que se ha creado exitosamente + if (onCreado) { + onCreado(nuevoEvento); + } + }; + + // Manejar el cierre del modal + const handleCerrar = () => { + if (onCerrar) { + onCerrar(); + } + }; + + // Si el componente se usa como modal controlado externamente + return ( + + + + ); +}; + +export default ModalCrearEvento; diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormaEmpleado.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormaActualizarEmpleado.jsx similarity index 64% rename from src/Vistas/Componentes/Organismos/Formularios/FormaEmpleado.jsx rename to src/Vistas/Componentes/Organismos/Formularios/FormaActualizarEmpleado.jsx index 2417e688..cb981ca6 100644 --- a/src/Vistas/Componentes/Organismos/Formularios/FormaEmpleado.jsx +++ b/src/Vistas/Componentes/Organismos/Formularios/FormaActualizarEmpleado.jsx @@ -4,13 +4,20 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import CampoTexto from '@Atomos/CampoTexto'; -const FormaEmpleado = ({ +// Límites para los campos +const LIMITE_NUMERO_EMERGENCIA = 10; +const LIMITE_AREA_TRABAJO = 50; +const LIMITE_POSICION = 50; +const LIMITE_CANTIDAD_PUNTOS = 10; +const MENSAJE_LIMITE = 'Máximo caracteres'; +const CAMPO_OBLIGATORIO = 'Este campo es obligatorio'; + +const FormaActualizarEmpleado = ({ datosEmpleado, erroresValidacion, manejarCambio, manejarAntiguedad, obtenerHelperText, - esEdicion, }) => { const estiloCuadricula = { display: 'flex', @@ -18,46 +25,6 @@ const FormaEmpleado = ({ }; return ( - - - - - - - - @@ -114,9 +89,13 @@ const FormaEmpleado = ({ required size='medium' error={!!erroresValidacion.posicion} - helperText={obtenerHelperText('posicion')} + helperText={ + erroresValidacion.posicion + ? CAMPO_OBLIGATORIO + : `${datosEmpleado.posicion.length}/${LIMITE_POSICION} - ${MENSAJE_LIMITE}` + } inputProps={{ - maxLength: 40, + maxLength: LIMITE_POSICION, }} /> @@ -126,16 +105,24 @@ const FormaEmpleado = ({ label='Cantidad de Puntos' name='cantidadPuntos' value={datosEmpleado.cantidadPuntos} - onChange={manejarCambio} + onChange={(num) => { + const soloNumeros = num.target.value.replace(/\D/g, ''); + manejarCambio({ target: { name: 'cantidadPuntos', value: soloNumeros } }); + }} required size='medium' error={!!erroresValidacion.cantidadPuntos} - helperText={obtenerHelperText('cantidadPuntos')} + helperText={ + erroresValidacion.cantidadPuntos + ? CAMPO_OBLIGATORIO + : `${ + (datosEmpleado.cantidadPuntos || '').toString().length + }/${LIMITE_CANTIDAD_PUNTOS} - ${MENSAJE_LIMITE}` + } inputProps={{ - maxLength: 10, - type: 'number', - min: 0, - step: 1, + maxLength: LIMITE_CANTIDAD_PUNTOS, + inputMode: 'numeric', + pattern: '[0-9]*', }} /> @@ -162,4 +149,4 @@ const FormaEmpleado = ({ ); }; -export default FormaEmpleado; +export default FormaActualizarEmpleado; diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx index 918b19f5..5b10c442 100644 --- a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx +++ b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx @@ -34,6 +34,7 @@ const FormaCrearCategorias = ({ const [rows, setRows] = useState([]); const { usuario } = useAuth(); const clienteSeleccionado = usuario.clienteSeleccionado; + useEffect(() => { const obtenerDatosProductos = async (clienteSeleccionado) => { diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearEmpleado.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearEmpleado.jsx new file mode 100644 index 00000000..e2911b14 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearEmpleado.jsx @@ -0,0 +1,339 @@ +import { Grid } from '@mui/material'; +import { DateField } from '@mui/x-date-pickers'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import CampoTexto from '@Atomos/CampoTexto'; +import CampoSelect from '@Atomos/CampoSelect'; + +// Límites para los campos +const LIMITE_NUMERO_EMERGENCIA = 10; +const LIMITE_NOMBRE_COMPLETO = 100; +const LIMITE_AREA_TRABAJO = 50; +const LIMITE_POSICION = 50; +const LIMITE_CORREO = 100; +const LIMITE_TELEFONO = 10; +const LIMITE_DIRECCION = 100; +const LIMITE_CONTRASENIA = 64; +const LIMITE_CANTIDAD_PUNTOS = 10; +const MENSAJE_LIMITE = 'Máximo caracteres'; +const CAMPO_OBLIGATORIO = 'Este campo es obligatorio'; + +const FormaCrearEmpleado = ({ + datosEmpleado, + erroresValidacion, + manejarCambio, + manejarAntiguedad, + manejarFechaNacimiento, + obtenerHelperText, +}) => { + const estiloCuadricula = { + display: 'flex', + justifyContent: 'center', + }; + return ( + + + { + const soloLetras = letra.target.value.replace(/[^a-zA-ZáéíóúÁÉÍÓÚñÑ\s]/g, ''); // solo letras y espacios + manejarCambio({ target: { name: 'nombreCompleto', value: soloLetras } }); + }} + required + size='medium' + error={!!erroresValidacion.nombreCompleto} + helperText={ + erroresValidacion.nombreCompleto + ? erroresValidacion.nombreCompleto === true + ? CAMPO_OBLIGATORIO + : erroresValidacion.nombreCompleto + : `${datosEmpleado.nombreCompleto.length}/${LIMITE_NOMBRE_COMPLETO} - ${MENSAJE_LIMITE}` + } + inputProps={{ + maxLength: LIMITE_NOMBRE_COMPLETO, + }} + /> + + + + + + + + + + + + + + + + + + { + const soloNumeros = num.target.value.replace(/\D/g, ''); // elimina todo lo que no es dígito + manejarCambio({ target: { name: 'numeroTelefono', value: soloNumeros } }); + }} + required + size='medium' + error={!!erroresValidacion.numeroTelefono} + helperText={ + erroresValidacion.numeroTelefono + ? erroresValidacion.numeroTelefono === true + ? CAMPO_OBLIGATORIO + : erroresValidacion.numeroTelefono + : `${datosEmpleado.numeroTelefono.length}/${LIMITE_TELEFONO} - ${MENSAJE_LIMITE}` + } + inputProps={{ + maxLength: LIMITE_TELEFONO, + inputMode: 'numeric', // para teclado numérico en móviles + pattern: '[0-9]*', + }} + /> + + + + + + + + + + + + + + + + + + + + { + if (!/^[a-zA-Z\s]*$/.test(event.key)) { + event.preventDefault(); + } + }} + onChange={manejarCambio} + required + size='medium' + error={!!erroresValidacion.areaTrabajo} + helperText={ + erroresValidacion.areaTrabajo + ? CAMPO_OBLIGATORIO + : `${datosEmpleado.areaTrabajo.length}/${LIMITE_AREA_TRABAJO} - ${MENSAJE_LIMITE}` + } + inputProps={{ + maxLength: LIMITE_AREA_TRABAJO, + }} + /> + + + + { + if (!/^[a-zA-Z\s]*$/.test(event.key)) { + event.preventDefault(); + } + }} + onChange={manejarCambio} + required + size='medium' + error={!!erroresValidacion.posicion} + helperText={ + erroresValidacion.posicion + ? CAMPO_OBLIGATORIO + : `${datosEmpleado.posicion.length}/${LIMITE_POSICION} - ${MENSAJE_LIMITE}` + } + inputProps={{ + maxLength: LIMITE_POSICION, + }} + /> + + + + { + const soloNumeros = num.target.value.replace(/\D/g, ''); + manejarCambio({ target: { name: 'cantidadPuntos', value: soloNumeros } }); + }} + required + size='medium' + error={!!erroresValidacion.cantidadPuntos} + helperText={ + erroresValidacion.cantidadPuntos + ? CAMPO_OBLIGATORIO + : `${datosEmpleado.cantidadPuntos.length}/${LIMITE_CANTIDAD_PUNTOS} - ${MENSAJE_LIMITE}` + } + inputProps={{ + maxLength: LIMITE_CANTIDAD_PUNTOS, + inputMode: 'numeric', + pattern: '[0-9]', + }} + /> + + + + + + + + + ); +}; + +export default FormaCrearEmpleado; diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearSetsProducto.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearSetsProducto.jsx new file mode 100644 index 00000000..8658f218 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearSetsProducto.jsx @@ -0,0 +1,197 @@ +import Alerta from '@Moleculas/Alerta'; +import CampoTexto from '@Atomos/CampoTexto'; +import { useState, useEffect } from 'react'; +import obtenerProductos from '@Servicios/obtenerProductos'; +import ProductosModal from '@Organismos/ProductosModal'; +import { useAuth } from '@Hooks/AuthProvider'; + +const columns = [ + { field: 'id', headerName: 'Id', width: 100 }, + { field: 'nombreProducto', headerName: 'Nombre', width: 220 }, + { field: 'tipo', headerName: 'Tipo', width: 100 }, +]; + +const LIMITE_NOMBRE = 50; +const LIMITE_NOMBRE_VISIBLE = 50; +const LIMITE_DESCRIPCION = 150; +const MENSAJE_LIMITE = 'Máximo caracteres'; + +const FormaCrearSetsProducto = ({ + nombreSetsProducto, + setNombreSetsProducto, + nombreVisible, + setNombreVisible, + descripcionSetsProducto, + setDescripcionSetsProducto, + productos, + setProductos, + erroresCampos, + setErroresCampos, + }) => { + const [rows, setRows] = useState([]); + const { usuario } = useAuth(); + const clienteSeleccionado = usuario.clienteSeleccionado; + + useEffect(() => { + const obtenerDatosProductos = async (clienteSeleccionado) => { + const productos = await obtenerProductos(clienteSeleccionado); + setRows(productos); + }; + + obtenerDatosProductos(clienteSeleccionado); + }, [clienteSeleccionado]); + + const handleClickFila = (evento) => { + const productoSeleccionado = evento.row; + + const yaExiste = productos.some((producto) => producto.id === productoSeleccionado.id); + if (!yaExiste) { + setProductos((prev) => [...prev, productoSeleccionado]); + } else { + setProductos((prev) => prev.filter((producto) => producto.id !== productoSeleccionado.id)); + } + + if (erroresCampos?.productos) { + setErroresCampos(prev => ({ ...prev, productos: false })); + } + }; + + const handleFilaSeleccion = (itemSeleccion) => { + const ids = Array.isArray(itemSeleccion) ? itemSeleccion : Array.from(itemSeleccion?.ids || []); + + const productosSeleccionados = ids + .map((id) => rows.find((row) => row.id === id)) + .filter((fila) => fila); + + setProductos(productosSeleccionados); + + if (erroresCampos?.productos && productosSeleccionados.length > 0) { + setErroresCampos(prev => ({ ...prev, productos: false })); + } + }; + + const handleNombreChange = (evento) => { + const value = evento.target.value; + if (value.trim() !== '') { + setNombreSetsProducto(value.slice(0, LIMITE_NOMBRE)); + } else if (nombreSetsProducto !== '') { + setNombreSetsProducto(''); + } + + if (erroresCampos?.nombre && value.trim()) { + setErroresCampos(prev => ({ ...prev, nombre: false })); + } + }; + + const handleNombreVisibleChange = (evento) => { + const value = evento.target.value; + if (value.trim() !== '') { + setNombreVisible(value.slice(0, LIMITE_NOMBRE_VISIBLE)); + } else if (nombreVisible !== '') { + setNombreVisible(''); + } + + if (erroresCampos?.nombreVisible && value.trim()) { + setErroresCampos(prev => ({ ...prev, nombreVisible: false })); + } + }; + + const handleDescripcionChange = (evento) => { + const value = evento.target.value; + if (value.trim() !== '') { + setDescripcionSetsProducto(value.slice(0, LIMITE_DESCRIPCION)); + } else if (descripcionSetsProducto !== '') { + setDescripcionSetsProducto(''); + } + + if (erroresCampos?.descripcion && value.trim()) { + setErroresCampos(prev => ({ ...prev, descripcion: false })); + } + }; + + return ( + <> + + + + + handleFilaSeleccion(ids)} + /> + + + + ); +}; + +export default FormaCrearSetsProducto; diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormularioActualizarUsuario.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormularioActualizarUsuario.jsx new file mode 100644 index 00000000..b6a3c5d7 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Formularios/FormularioActualizarUsuario.jsx @@ -0,0 +1,251 @@ +import { useEffect, useRef } from 'react'; +import { Box, Grid } from '@mui/material'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { DateField } from '@mui/x-date-pickers/DateField'; +import CampoTexto from '@Atomos/CampoTexto'; +import CampoSelect from '@Atomos/CampoSelect'; +import CampoSelectMultiple from '@Atomos/CampoSelectMultiple'; + +const FormularioActualizarUsuario = ({ + datosUsuario, + erroresValidacion, + manejarCambio, + manejarFechaNacimiento, + roles = [], + clientes = [], + cargandoRoles = false, + CAMPO_OBLIGATORIO = 'Este campo es obligatorio', +}) => { + const estiloCuadricula = { + display: 'flex', + justifyContent: 'center', + }; + + const rolAnterior = useRef(null); + useEffect(() => { + if (rolAnterior.current === 1 && datosUsuario.rol !== 1) { + manejarCambio({ + target: { + name: 'cliente', + value: [], + }, + }); + } + if (datosUsuario.rol === 1 && clientes.length > 0) { + manejarCambio({ + target: { + name: 'cliente', + value: clientes.map((cliente) => cliente.idCliente), + }, + }); + } + rolAnterior.current = datosUsuario.rol; + }, [datosUsuario.rol, clientes, manejarCambio]); + + return ( + + + { + const soloLetras = letra.target.value.replace(/[^a-zA-ZáéíóúÁÉÍÓÚñÑ\s]/g, ''); + manejarCambio({ target: { name: 'nombreCompleto', value: soloLetras } }); + }} + required + size='medium' + error={!!erroresValidacion.nombreCompleto} + helperText={erroresValidacion.nombreCompleto && CAMPO_OBLIGATORIO} + inputProps={{ + maxLength: 50, + }} + /> + + + + + + + + + + + + + + { + const soloNumeros = num.target.value.replace(/\D/g, ''); + manejarCambio({ target: { name: 'numeroTelefono', value: soloNumeros } }); + }} + required + size='medium' + error={!!erroresValidacion.numeroTelefono} + helperText={ + erroresValidacion.numeroTelefono === true + ? CAMPO_OBLIGATORIO + : erroresValidacion.numeroTelefono || '' + } + inputProps={{ + maxLength: 10, + }} + /> + + + + + + rol.idRol !== 3) + .map((rol) => ({ + value: rol.idRol, + label: rol.nombre, + }))} + disabled={cargandoRoles} + /> + + + + + + ({ + value: cliente.idCliente, + label: cliente.nombreComercial, + }))} + //disabled={esSuperAdmin} + /> + + + + + + + + + ); +}; + +export default FormularioActualizarUsuario; diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormularioCrearEvento.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormularioCrearEvento.jsx new file mode 100644 index 00000000..b15534e6 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Formularios/FormularioCrearEvento.jsx @@ -0,0 +1,122 @@ +// RF36 - Crear Evento - [https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF36] + +import Alerta from '@Moleculas/Alerta'; +import CampoTexto from '@Atomos/CampoTexto'; +import { Box } from '@mui/material'; +import CampoSelect from '../../Atomos/CampoSelect'; + +// Constantes para mensajes y límites +const LIMITE_NOMBRE = 100; +const LIMITE_DESCRIPCION = 300; +const MENSAJE_LIMITE = 'Máximo caracteres'; + +const FormularioCrearEvento = ({ + nombreEvento, + setNombreEvento, + nombreError = false, + descripcionEvento, + setDescripcionEvento, + descripcionError = false, + puntosEvento, + setPuntosEvento, + puntosError = false, + multiplicadorEvento, + setMultiplicadorEvento, + multiplicadorError = false, + mostrarAlerta, + setMostrarAlerta, +}) => { + // Validar si el valor es un número válido + const esNumeroValido = (valor) => { + const regex = /^[0-9]+(\.[0-9]+)?$/; + return regex.test(valor) || valor === ''; + }; + + // Manejar el cambio de nombre + const manejarCambioNombre = (evento) => setNombreEvento(evento.target.value); + + // Manejar el cambio de descripción + const manejarCambioDescripcion = (evento) => setDescripcionEvento(evento.target.value); + + // Manejar el cambio de puntos + const manejarCambioPuntos = (evento) => { + const valor = evento.target.value; + if (esNumeroValido(valor) || valor === null) { + setPuntosEvento(valor); + } + }; + + // Manejar el cambio de multiplicador + const manejarCambioMultiplicador = (evento) => { + const valor = evento.target.value; + if (esNumeroValido(valor) || valor === null) { + setMultiplicadorEvento(valor); + } + }; + + return ( + <> + + + + + + + {mostrarAlerta && ( + setMostrarAlerta(false)} + sx={{ mb: 2, mt: 2 }} + /> + )} + + ); +}; + +export default FormularioCrearEvento; diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormularioCrearUsuario.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormularioCrearUsuario.jsx index 74bc8678..59f05533 100644 --- a/src/Vistas/Componentes/Organismos/Formularios/FormularioCrearUsuario.jsx +++ b/src/Vistas/Componentes/Organismos/Formularios/FormularioCrearUsuario.jsx @@ -19,7 +19,7 @@ const LIMITE_APELLIDO = 50; const LIMITE_CORREO = 100; const LIMITE_TELEFONO = 10; const LIMITE_DIRECCION = 100; -const LIMITE_CONTRASENIA = 64; +const LIMITE_CONTRASENIA = 64; const MENSAJE_LIMITE = 'Máximo caracteres'; const FormularioCrearUsuario = ({ open, onClose, onUsuarioCreado }) => { @@ -85,7 +85,7 @@ const FormularioCrearUsuario = ({ open, onClose, onUsuarioCreado }) => { setAlerta({ tipo: 'success', mensaje: resumenUsuario, - icono: true, + icono: true, cerrable: true, centradoInferior: true, duracion: 3000, @@ -172,7 +172,7 @@ const FormularioCrearUsuario = ({ open, onClose, onUsuarioCreado }) => { size='medium' error={!!errores.nombreCompleto} helperText={ - errores.nombreCompleto + errores.nombreCompleto ? CAMPO_OBLIGATORIO : `${datosUsuario.nombreCompleto.length}/${LIMITE_NOMBRE} - ${MENSAJE_LIMITE}` } @@ -192,7 +192,7 @@ const FormularioCrearUsuario = ({ open, onClose, onUsuarioCreado }) => { size='medium' error={!!errores.apellido} helperText={ - errores.apellido + errores.apellido ? CAMPO_OBLIGATORIO : `${datosUsuario.apellido.length}/${LIMITE_APELLIDO} - ${MENSAJE_LIMITE}` } @@ -253,7 +253,9 @@ const FormularioCrearUsuario = ({ open, onClose, onUsuarioCreado }) => { error={!!errores.correoElectronico} helperText={ errores.correoElectronico - ? (errores.correoElectronico === true ? CAMPO_OBLIGATORIO : errores.correoElectronico) + ? errores.correoElectronico === true + ? CAMPO_OBLIGATORIO + : errores.correoElectronico : `${datosUsuario.correoElectronico.length}/${LIMITE_CORREO} - ${MENSAJE_LIMITE}` } inputProps={{ @@ -264,28 +266,29 @@ const FormularioCrearUsuario = ({ open, onClose, onUsuarioCreado }) => { { - const soloNumeros = num.target.value.replace(/\D/g, ''); // elimina todo lo que no es dígito - manejarCambio({ target: { name: 'numeroTelefono', value: soloNumeros } }); - }} - required - size='medium' - error={!!errores.numeroTelefono} - helperText={ - errores.numeroTelefono - ? (errores.numeroTelefono === true ? CAMPO_OBLIGATORIO : errores.numeroTelefono) - : `${datosUsuario.numeroTelefono.length}/${LIMITE_TELEFONO} - ${MENSAJE_LIMITE}` - } - inputProps={{ - maxLength: LIMITE_TELEFONO, - inputMode: 'numeric', // para teclado numérico en móviles - pattern: '[0-9]*', - }} - /> - + label='Número de Teléfono' + name='numeroTelefono' + value={datosUsuario.numeroTelefono} + onChange={(num) => { + const soloNumeros = num.target.value.replace(/\D/g, ''); // elimina todo lo que no es dígito + manejarCambio({ target: { name: 'numeroTelefono', value: soloNumeros } }); + }} + required + size='medium' + error={!!errores.numeroTelefono} + helperText={ + errores.numeroTelefono + ? errores.numeroTelefono === true + ? CAMPO_OBLIGATORIO + : errores.numeroTelefono + : `${datosUsuario.numeroTelefono.length}/${LIMITE_TELEFONO} - ${MENSAJE_LIMITE}` + } + inputProps={{ + maxLength: LIMITE_TELEFONO, + inputMode: 'numeric', // para teclado numérico en móviles + pattern: '[0-9]*', + }} + /> @@ -298,7 +301,7 @@ const FormularioCrearUsuario = ({ open, onClose, onUsuarioCreado }) => { size='medium' error={!!errores.direccion} helperText={ - errores.direccion + errores.direccion ? CAMPO_OBLIGATORIO : `${datosUsuario.direccion.length}/${LIMITE_DIRECCION} - ${MENSAJE_LIMITE}` } @@ -359,13 +362,14 @@ const FormularioCrearUsuario = ({ open, onClose, onUsuarioCreado }) => { autoComplete='new-password' helperText={ errores.contrasenia - ? (errores.contrasenia === true ? CAMPO_OBLIGATORIO : errores.contrasenia) + ? errores.contrasenia === true + ? CAMPO_OBLIGATORIO + : errores.contrasenia : `${datosUsuario.contrasenia.length}/${LIMITE_CONTRASENIA} - ${MENSAJE_LIMITE}` } inputProps={{ maxLength: LIMITE_CONTRASENIA, }} - /> @@ -382,7 +386,9 @@ const FormularioCrearUsuario = ({ open, onClose, onUsuarioCreado }) => { autoComplete='new-password' helperText={ errores.confirmarContrasenia - ? (errores.confirmarContrasenia === true ? CAMPO_OBLIGATORIO : errores.confirmarContrasenia) + ? errores.confirmarContrasenia === true + ? CAMPO_OBLIGATORIO + : errores.confirmarContrasenia : `${datosUsuario.confirmarContrasenia.length}/${LIMITE_CONTRASENIA} - ${MENSAJE_LIMITE}` } inputProps={{ diff --git a/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx new file mode 100644 index 00000000..0dcef6ba --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx @@ -0,0 +1,242 @@ +import React, { useState, useEffect, useRef } from 'react'; +import Grid from '@mui/material/Grid'; +import List from '@mui/material/List'; +import Card from '@mui/material/Card'; +import CardHeader from '@mui/material/CardHeader'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import Checkbox from '@mui/material/Checkbox'; +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import KeyboardDoubleArrowRightIcon from '@mui/icons-material/KeyboardDoubleArrowRight'; +import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; +import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft'; +import KeyboardDoubleArrowLeftIcon from '@mui/icons-material/KeyboardDoubleArrowLeft'; + + +// Funciones utilitarias +function no(no1, no2, funcionClave = (elemento) => elemento.id || elemento) { + return no1.filter((valorA) => !no2.some((valorB) => funcionClave(valorA) === funcionClave(valorB))); +} + +function interseccion(inter1, inter2, funcionClave = (elemento) => elemento.id || elemento) { + return inter1.filter((valorA) => inter2.some((valorB) => funcionClave(valorA) === funcionClave(valorB))); +} + +function union(union1, union2, funcionClave = (elemento) => elemento.id || elemento) { + return [...union1, ...no(union2, union1, funcionClave)]; +} + +// Componente Lista de Transferencia Personalizada +const ListaTransferenciaPersonalizada = ({ + elementosDisponibles = [], + elementosSeleccionados = [], + alCambiarSeleccion, + tituloIzquierda = "Disponibles", + tituloDerecha = "Seleccionados", + obtenerEtiquetaElemento = (elemento) => elemento.etiqueta || elemento.nombre || String(elemento), + obtenerClaveElemento = (elemento) => elemento.id || elemento.clave || elemento, + deshabilitado = false, + alturaMaxima = 357, + ancho = 350 + }) => { + const [marcados, setMarcados] = useState([]); + const [izquierda, setIzquierda] = useState(elementosDisponibles); + const [derecha, setDerecha] = useState(elementosSeleccionados); + + const updatingFromPropsRef = useRef(false); + + // Actualizar estado interno cuando cambien las props + useEffect(() => { + updatingFromPropsRef.current = true; + setIzquierda(elementosDisponibles); + updatingFromPropsRef.current = false; + }, [elementosDisponibles]); + + useEffect(() => { + updatingFromPropsRef.current = true; + setDerecha(elementosSeleccionados); + updatingFromPropsRef.current = false; + }, [elementosSeleccionados]); + + useEffect(() => { + if (alCambiarSeleccion && !updatingFromPropsRef.current) { + alCambiarSeleccion({ + disponibles: izquierda, + seleccionados: derecha + }); + } + }, [izquierda, derecha, alCambiarSeleccion]); + + const marcadosIzquierda = interseccion(marcados, izquierda, obtenerClaveElemento); + const marcadosDerecha = interseccion(marcados, derecha, obtenerClaveElemento); + + const manejarAlternar = (valor) => () => { + const indiceActual = marcados.findIndex(elemento => obtenerClaveElemento(elemento) === obtenerClaveElemento(valor)); + const nuevosMarcados = [...marcados]; + + if (indiceActual === -1) { + nuevosMarcados.push(valor); + } else { + nuevosMarcados.splice(indiceActual, 1); + } + + setMarcados(nuevosMarcados); + }; + + const numeroDeMarcados = (elementos) => interseccion(marcados, elementos, obtenerClaveElemento).length; + + const manejarAlternarTodos = (elementos) => () => { + if (numeroDeMarcados(elementos) === elementos.length) { + setMarcados(no(marcados, elementos, obtenerClaveElemento)); + } else { + setMarcados(union(marcados, elementos, obtenerClaveElemento)); + } + }; + + const manejarMarcadosADerecha = () => { + setDerecha(union(derecha, marcadosIzquierda, obtenerClaveElemento)); + setIzquierda(no(izquierda, marcadosIzquierda, obtenerClaveElemento)); + setMarcados(no(marcados, marcadosIzquierda, obtenerClaveElemento)); + }; + + const manejarMarcadosAIzquierda = () => { + setIzquierda(union(izquierda, marcadosDerecha, obtenerClaveElemento)); + setDerecha(no(derecha, marcadosDerecha, obtenerClaveElemento)); + setMarcados(no(marcados, marcadosDerecha, obtenerClaveElemento)); + }; + + const manejarTodoADerecha = () => { + setDerecha(union(derecha, izquierda, obtenerClaveElemento)); + setIzquierda([]); + setMarcados([]); + }; + + const manejarTodoAIzquierda = () => { + setIzquierda(union(izquierda, derecha, obtenerClaveElemento)); + setDerecha([]); + setMarcados([]); + }; + + const listaPersonalizada = (titulo, elementos) => ( + + + } + title={titulo} + subheader={`${numeroDeMarcados(elementos)}/${elementos.length} seleccionados`} + /> + + + {elementos.map((elemento) => { + const claveElemento = obtenerClaveElemento(elemento); + const etiquetaElemento = obtenerEtiquetaElemento(elemento); + const estaMarcado = marcados.some(elementoMarcado => obtenerClaveElemento(elementoMarcado) === claveElemento); + + return ( + + + + + + + ); + })} + + + ); + + return ( + + {listaPersonalizada(tituloIzquierda, izquierda)} + + + + + + + + + {listaPersonalizada(tituloDerecha, derecha)} + + ); +}; + +export default ListaTransferenciaPersonalizada; \ No newline at end of file diff --git a/src/Vistas/Componentes/Organismos/ModalActualizarCategoria.jsx b/src/Vistas/Componentes/Organismos/ModalActualizarCategoria.jsx new file mode 100644 index 00000000..dd9522d0 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalActualizarCategoria.jsx @@ -0,0 +1,94 @@ +// RF48 Actualizar categoría de productos - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF48 +import { useState, useEffect, useCallback } from 'react'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import FormaCrearCategorias from '@Organismos/Formularios/FormaCrearCategoria'; +import Alerta from '@Moleculas/Alerta'; +import { useActualizarCategoria } from '@Hooks/Categorias/useActualizarCategoria'; + +const ModalActualizarCategoria = ({ open, onClose, categoria, onActualizado }) => { + const [nombreCategoria, setNombreCategoria] = useState(''); + const [descripcionCategoria, setDescripcionCategoria] = useState(''); + const [productos, setProductos] = useState([]); + const [mostrarAlerta, setMostrarAlerta] = useState(false); + + const { actualizar, cargando, error, mensaje } = useActualizarCategoria(); + + // Precargar datos al abrir el modal + useEffect(() => { + if (open && categoria) { + setNombreCategoria(categoria.nombreCategoria || ''); + setDescripcionCategoria(categoria.descripcion || ''); + + // Asegurarse de transformar productos a IDs si vienen como objetos + const productosParseados = categoria.productos?.map((producto) => + typeof producto === 'object' ? producto.idProducto : producto); + setProductos(productosParseados || []); + } + }, [open, categoria]); + + const handleConfirmar = async () => { + if (!nombreCategoria.trim() || productos.length === 0) { + setMostrarAlerta(true); + return; + } + + const exito = await actualizar(categoria.idCategoria, { + nombreCategoria: nombreCategoria.trim(), + descripcion: descripcionCategoria.trim(), + productos, + }); + + if (exito) { + onActualizado?.(); + onClose(); + } + + if (!error) { + onActualizado?.(); + onClose(); + } + }; + + + + + + + const handleCerrar = useCallback(() => { + onClose(); + }, [onClose]); + + return ( + + + {mensaje && ( + + )} + + ); +}; + +export default ModalActualizarCategoria; \ No newline at end of file diff --git a/src/Vistas/Componentes/Organismos/ModalEmpleados.jsx b/src/Vistas/Componentes/Organismos/ModalActualizarEmpleado.jsx similarity index 78% rename from src/Vistas/Componentes/Organismos/ModalEmpleados.jsx rename to src/Vistas/Componentes/Organismos/ModalActualizarEmpleado.jsx index fb0be117..e577113a 100644 --- a/src/Vistas/Componentes/Organismos/ModalEmpleados.jsx +++ b/src/Vistas/Componentes/Organismos/ModalActualizarEmpleado.jsx @@ -3,22 +3,22 @@ import { Box } from '@mui/material'; import Alerta from '@Moleculas/Alerta'; import ModalFlotante from '@Organismos/ModalFlotante'; -import FormaEmpleado from '@Organismos/Formularios/FormaEmpleado'; -import { useAccionesEmpleado } from '@Hooks/Empleados/useAccionesEmpleado'; +import FormaActualizarEmpleado from '@Organismos/Formularios/FormaActualizarEmpleado'; +import { useActualizarEmpleado } from '@SRC/hooks/Empleados/useActualizarEmpleado'; -const ModalEmpleados = ({ open, onClose, onAccion, empleadoEdicion }) => { +const ModalActualizarEmpleado = ({ open, onClose, onAccion, empleadoEdicion }) => { const { datosEmpleado, erroresValidacion, alerta, setAlerta, - esEdicion, manejarCambio, manejarAntiguedad, obtenerHelperText, handleGuardar, limpiarFormulario, - } = useAccionesEmpleado(empleadoEdicion); + cargando, + } = useActualizarEmpleado(empleadoEdicion); const manejarConfirmacion = async () => { const resultado = await handleGuardar(); @@ -26,9 +26,7 @@ const ModalEmpleados = ({ open, onClose, onAccion, empleadoEdicion }) => { if (resultado?.exito) { if (onAccion) await onAccion(); - if (!esEdicion) { - limpiarFormulario(); - } + limpiarFormulario(); // Esperar un momento para que el usuario vea el mensaje de éxito setTimeout(() => { @@ -47,7 +45,8 @@ const ModalEmpleados = ({ open, onClose, onAccion, empleadoEdicion }) => { open={open} onClose={manejarCierre} onConfirm={manejarConfirmacion} - titulo={esEdicion ? datosEmpleado.nombreCompleto : 'Agregar Empleado'} + titulo={datosEmpleado.nombreCompleto} + loading={cargando} > { noValidate autoComplete='off' > - @@ -83,4 +81,4 @@ const ModalEmpleados = ({ open, onClose, onAccion, empleadoEdicion }) => { ); }; -export default ModalEmpleados; +export default ModalActualizarEmpleado; diff --git a/src/Vistas/Componentes/Organismos/ModalActualizarUsuario.jsx b/src/Vistas/Componentes/Organismos/ModalActualizarUsuario.jsx new file mode 100644 index 00000000..2f0cfd86 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalActualizarUsuario.jsx @@ -0,0 +1,95 @@ +import { Box } from '@mui/material'; +import Alerta from '@Moleculas/Alerta'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import FormularioActualizarUsuario from '@Organismos/Formularios/FormularioActualizarUsuario'; +import { useAccionesUsuario } from '@Hooks/Usuarios/useAccionesUsuario'; + +const ModalActualizarUsuario = ({ + open, + onClose, + onAccion, + usuarioEdicion, + roles = [], + clientes = [], + esSuperAdmin = false, + cargandoRoles = false, +}) => { + const { + datosUsuario, + erroresValidacion, + setAlerta, + alerta, + manejarCambio, + manejarFechaNacimiento, + obtenerHelperText, + handleGuardar, + cargando, + esEdicion, + limpiarFormulario, + } = useAccionesUsuario(usuarioEdicion); + + const manejarConfirmacion = async () => { + const resultado = await handleGuardar(); + if (resultado?.exito) { + if (onAccion) await onAccion(true, resultado?.mensaje || 'Usuario actualizado exitosamente'); + setTimeout(() => { + limpiarFormulario(); + onClose(); + }, 1000); + } + }; + + const manejarCierre = () => { + setAlerta(null); + onClose(); + }; + + return ( + <> + + + + + + + {alerta && ( + setAlerta(null)} + centradoInferior + /> + )} + + ); +}; + +export default ModalActualizarUsuario; diff --git a/src/Vistas/Componentes/Organismos/ModalCrearEmpleado.jsx b/src/Vistas/Componentes/Organismos/ModalCrearEmpleado.jsx new file mode 100644 index 00000000..e8cb85ee --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalCrearEmpleado.jsx @@ -0,0 +1,83 @@ +//RF16 - Agregar empleado - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF16 +//RF19 - Actualizar empleado - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF19 +import { Box } from '@mui/material'; +import Alerta from '@Moleculas/Alerta'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import FormaCrearEmpleado from '@Organismos/Formularios/FormaCrearEmpleado'; +import { useCrearEmpleado } from '@SRC/hooks/Empleados/useCrearEmpleado'; + +const ModalCrearEmpleado = ({ open, onClose, onAccion }) => { + const { + datosEmpleado, + erroresValidacion, + alerta, + setAlerta, + manejarCambio, + manejarFechaNacimiento, + manejarAntiguedad, + obtenerHelperText, + handleGuardar, + cargando, + } = useCrearEmpleado(); + + const manejarConfirmacion = async () => { + const resultado = await handleGuardar(); + + if (resultado?.exito) { + if (onAccion) await onAccion(); + + // Esperar un momento para que el usuario vea el mensaje de éxito + setTimeout(() => { + onClose(); + }, 1500); + } + }; + + const manejarCierre = () => { + setAlerta(null); + onClose(); + }; + + return ( + + + + + + {alerta && ( + setAlerta(null)} + /> + )} + + ); +}; + +export default ModalCrearEmpleado; diff --git a/src/Vistas/Componentes/Organismos/ModalCrearSetsProductos.jsx b/src/Vistas/Componentes/Organismos/ModalCrearSetsProductos.jsx new file mode 100644 index 00000000..e5e0d410 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalCrearSetsProductos.jsx @@ -0,0 +1,170 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import ModalFlotante from '@Organismos/ModalFlotante.jsx'; +import FormaCrearSetsProducto from '@Organismos/Formularios/FormaCrearSetsProducto.jsx'; +import useCrearSetsProducto from '@Hooks/SetsProductos/useCrearSetsProducto'; +import Alerta from '@Moleculas/Alerta'; + +const ModalCrearSetsProductos = ({ abierto = false, onCerrar, onCreado }) => { + const [nombreSetsProducto, setNombreSetsProducto] = useState(''); + const [nombreVisible, setNombreVisible] = useState(''); + const [descripcionSetsProducto, setDescripcionSetsProducto] = useState(''); + const [productos, setProductos] = useState([]); + const [mostrarAlerta, setMostrarAlerta] = useState(false); + const [mensajeAlerta, setMensajeAlerta] = useState(''); + const [erroresCampos, setErroresCampos] = useState({ + nombre: false, + nombreVisible: false, + descripcion: false, + productos: false + }); + + const tieneReseteo = useRef(false); + + const { crearSetsProducto, cargando, exito, error, mensaje, setError, resetEstado } + = useCrearSetsProducto(); + + useEffect(() => { + if (!abierto && !tieneReseteo.current) { + tieneReseteo.current = true; + resetEstado(); + setTimeout(() => { + setNombreSetsProducto(''); + setNombreVisible(''); + setDescripcionSetsProducto(''); + setProductos([]); + setMostrarAlerta(false); + setErroresCampos({ + nombre: false, + nombreVisible: false, + descripcion: false, + productos: false + }); + }, 0); + } else if (abierto) { + tieneReseteo.current = false; + } + }, [abierto, resetEstado]); + + useEffect(() => { + let timeoutId; + if (exito) { + timeoutId = setTimeout(() => { + if (onCreado) { + onCreado(); + } else { + onCerrar(); + } + }, 2000); + } + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [exito, onCreado, onCerrar]); + + const handleCerrar = useCallback(() => { + onCerrar(); + }, [onCerrar]); + + const validarCampos = () => { + const errores = { + nombre: !nombreSetsProducto.trim(), + nombreVisible: !nombreVisible.trim(), + descripcion: !descripcionSetsProducto.trim(), + productos: productos.length === 0 + }; + + setErroresCampos(errores); + + let mensaje = ''; + if (errores.productos) { + mensaje = 'Debe seleccionar al menos un producto.'; + } else if (errores.nombre || errores.nombreVisible || errores.descripcion) { + mensaje = 'Por favor complete todos los campos obligatorios.'; + } + + return { tieneErrores: Object.values(errores).some(error => error), mensaje }; + }; + + const handleConfirmar = async () => { + const { tieneErrores, mensaje } = validarCampos(); + + if (tieneErrores) { + setMostrarAlerta(true); + setMensajeAlerta(mensaje); + return; + } + + setMostrarAlerta(false); + setMensajeAlerta(''); + setErroresCampos({ + nombre: false, + nombreVisible: false, + descripcion: false, + productos: false + }); + + await crearSetsProducto({ + nombre: nombreSetsProducto.trim(), + nombreVisible: nombreVisible.trim(), + descripcion: descripcionSetsProducto.trim(), + productos, + }); + }; + + return (<> + + + + + {mostrarAlerta && !exito && !error && ( + setMostrarAlerta(false)} + centradoInferior + /> + )} + + {(exito || error) && ( + setError(false) : undefined} + centradoInferior + /> + )} + + ); +}; + +export default ModalCrearSetsProductos; \ No newline at end of file diff --git a/src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx b/src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx new file mode 100644 index 00000000..4173f8d8 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx @@ -0,0 +1,319 @@ +// RF[8] Leer Rol - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF8 + +import { useEffect, useState, useRef, useCallback } from 'react'; +import { Box, Typography, useTheme } from '@mui/material'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import { useLeerRol } from '@Hooks/Roles/useLeerRol'; +import useActualizarRol from '@Hooks/Roles/useActualizarRol'; +import Alerta from '@Moleculas/Alerta'; +import Tabla from '@Organismos/Tabla'; +import ListaTransferencia from '@Organismos/ListaTransferencia'; +import CampoTexto from '@Atomos/CampoTexto'; +import obtenerPermisos from '@Servicios/obtenerPermisos'; +import { tokens } from '@SRC/theme'; + +const ModalDetalleRol = ({ abierto, onCerrar, idRol }) => { + const { detalle, cargando, error, leerRol } = useLeerRol(); + const { actualizarRol, cargando: cargandoActualizacion, error: errorActualizacion, exitoso, mensaje, limpiarEstado } = useActualizarRol(); + const theme = useTheme(); + const colores = tokens(theme.palette.mode); + const [modoEdicion, setModoEdicion] = useState(false); + const [permisosDisponibles, setPermisosDisponibles] = useState([]); + const [permisosSeleccionados, setPermisosSeleccionados] = useState([]); + + const [nombreRol, setNombreRol] = useState(''); + const [descripcionRol, setDescripcionRol] = useState(''); + + const [nombreOriginal, setNombreOriginal] = useState(''); + const [descripcionOriginal, setDescripcionOriginal] = useState(''); + + const permisosLoadedRef = useRef(false); + + useEffect(() => { + if (abierto && idRol) leerRol(idRol); + }, [abierto, idRol, leerRol]); + + useEffect(() => { + if (detalle) { + const nombre = detalle.nombre || ''; + const descripcion = detalle.descripcion || ''; + + setNombreRol(nombre); + setDescripcionRol(descripcion); + + setNombreOriginal(nombre); + setDescripcionOriginal(descripcion); + } + }, [detalle]); + + useEffect(() => { + if (!abierto || !modoEdicion) { + permisosLoadedRef.current = false; + } + }, [abierto, modoEdicion]); + + useEffect(() => { + const cargarPermisos = async () => { + if (detalle && modoEdicion && !permisosLoadedRef.current) { + const todosLosPermisos = await obtenerPermisos(); + const permisosDelRol = detalle.permisos || []; + + const permisosNoAsignados = todosLosPermisos.filter( + permiso => !permisosDelRol.some(asignado => asignado.id === permiso.id) + ); + + setPermisosDisponibles(permisosNoAsignados); + setPermisosSeleccionados(permisosDelRol); + permisosLoadedRef.current = true; + } + }; + + cargarPermisos(); + }, [detalle, modoEdicion]); + + const columnas = [ + { + field: 'nombre', + headerName: 'Permisos', + flex: 1, + }, + { + field: 'descripcion', + headerName: 'Descripción', + flex: 2, + }, + ]; + + const filas = (detalle?.permisos || []).map((permiso) => ({ + id: permiso.id, + nombre: permiso.nombre, + descripcion: permiso.descripcion, + })); + + const manejarCambioEdicion = () => { + setModoEdicion(!modoEdicion); + // Limpiar estados de actualización al entrar en modo edición + if (!modoEdicion) { + limpiarEstado(); + } + }; + + const puedeEditarse = idRol !== 1 && idRol !== 3; + + const manejarCambioTransferencia = useCallback(({ disponibles, seleccionados }) => { + setPermisosDisponibles(disponibles); + setPermisosSeleccionados(seleccionados); + }, []); + + const manejarGuardar = async () => { + const datosParaEnviar = { + nombre: nombreRol !== nombreOriginal ? nombreRol : null, + descripcion: descripcionRol !== descripcionOriginal ? descripcionRol : null, + permisos: permisosSeleccionados.map(permiso => permiso.id) + }; + + const resultado = await actualizarRol(idRol, datosParaEnviar); + + if (resultado.success) { + setModoEdicion(false); + leerRol(idRol); + } + }; + + const manejarCancelar = () => { + setNombreRol(nombreOriginal); + setDescripcionRol(descripcionOriginal); + setModoEdicion(false); + limpiarEstado(); + }; + + const manejarCerrar = () => { + setModoEdicion(false); + limpiarEstado(); + onCerrar(); + }; + + const manejarCambioNombre = (event) => { + setNombreRol(event.target.value); + }; + + const manejarCambioDescripcion = (event) => { + setDescripcionRol(event.target.value); + }; + + const botonesModo = modoEdicion ? [ + { + label: 'Cancelar', + variant: 'outlined', + size: 'large', + outlineColor: colores.altertex[1], + onClick: manejarCancelar, + disabled: cargandoActualizacion, + }, + { + label: 'Guardar', + variant: 'contained', + size: 'large', + backgroundColor: colores.altertex[1], + onClick: manejarGuardar, + disabled: cargandoActualizacion, + } + ] : [ + ...(puedeEditarse ? [{ + label: 'Editar', + variant: 'contained', + size: 'large', + backgroundColor: colores.altertex[1], + onClick: manejarCambioEdicion, + }] : []), + { + label: 'Salir', + variant: 'outlined', + size: 'large', + outlineColor: colores.altertex[1], + onClick: manejarCerrar, + } + ]; + + return ( + <> + {(error || errorActualizacion) && ( + + {}} + /> + + )} + + {exitoso && mensaje && ( + + + + )} + + + {(cargando || cargandoActualizacion) && ( + + {cargandoActualizacion ? 'Guardando cambios...' : 'Cargando...'} + + )} + + {!cargando && detalle && ( + <> + {modoEdicion ? ( + + + + + + + ) : ( + + + Rol: {detalle.nombre} + + + Descripción: {detalle.descripcion} + + + )} + + + Número de usuarios asociados a este rol: {detalle.totalUsuarios} + + + {modoEdicion ? ( + + + Gestionar Permisos + + permiso.nombre} + obtenerClaveElemento={(permiso) => permiso.id} + alturaMaxima={300} + ancho={300} + disabled={cargandoActualizacion} + /> + + ) : ( + + + + )} + + )} + + + ); +}; + +export default ModalDetalleRol; \ No newline at end of file diff --git a/src/Vistas/Componentes/Organismos/ModalEditarCategoria.jsx b/src/Vistas/Componentes/Organismos/ModalEditarCategoria.jsx new file mode 100644 index 00000000..d47ed2be --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalEditarCategoria.jsx @@ -0,0 +1,108 @@ +import React, { useState, useCallback } from 'react'; +import { Box, Button, useTheme } from '@mui/material'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import CategoriaInfoEditable from '@Organismos/CategoriaInfoEditable'; +import { tokens } from '@SRC/theme'; + +/** + * Modal para editar una categoría existente. + * + * RF49 - Actualizar Categoría + */ +const ModalEditarCategoria = ({ + abierto, + onCerrar, + categoria, + onGuardar, + onCambioTransferencia, + estadoActualizacion, + setCategoria, +}) => { + const theme = useTheme(); + const colores = tokens(theme.palette.mode); + + const [errorNombre, setErrorNombre] = useState(false); + + const manejarCambioNombre = useCallback((evento) => { + const nuevoNombre = evento.target.value; + setCategoria(prev => ({ + ...prev, + nombreCategoria: nuevoNombre + })); + if (nuevoNombre.trim()) { + setErrorNombre(false); + } + }, [setCategoria]); + + const manejarCambioDescripcion = useCallback((evento) => { + const nuevaDescripcion = evento.target.value; + setCategoria(prev => ({ + ...prev, + descripcion: nuevaDescripcion + })); + }, [setCategoria]); + + const manejarGuardar = useCallback(() => { + if (!categoria?.nombreCategoria?.trim()) { + setErrorNombre(true); + return; + } + setErrorNombre(false); + onGuardar(); + }, [categoria?.nombreCategoria, onGuardar]); + + if (!categoria) return null; + + const { cargando } = estadoActualizacion || {}; + + return ( + + + + + + + + + ); +}; + +export default ModalEditarCategoria; \ No newline at end of file diff --git a/src/Vistas/Componentes/Organismos/ModalEditarPedido.jsx b/src/Vistas/Componentes/Organismos/ModalEditarPedido.jsx new file mode 100644 index 00000000..7d378da1 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalEditarPedido.jsx @@ -0,0 +1,306 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Box, Button, useTheme } from '@mui/material'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import CampoTexto from '@Atomos/CampoTexto'; +import Alerta from '@Moleculas/Alerta'; +import { useActualizarPedido } from '@Hooks/Pedidos/useActualizarPedido'; +import { tokens } from '@SRC/theme'; + +// Constantes para los límites de caracteres +const LIMITE_ESTATUS = 50; +const MENSAJE_LIMITE = 'Máximo caracteres'; + +// Expresiones regulares para validación +const REGEX_PRECIO = /^\d{1,8}(\.\d{0,2})?$/; +const REGEX_SOLO_LETRAS = /^[A-Za-záéíóúÁÉÍÓÚñÑ\s]*$/; + +const ModalEditarPedido = ({ + abierto, + onCerrar, + datosIniciales, + onActualizar, + onMostrarAlerta, +}) => { + const theme = useTheme(); + const colores = tokens(theme.palette.mode); + const { actualizarPedido, cargando } = useActualizarPedido(); + + const [pedido, setPedido] = useState({}); + const [errores, setErrores] = useState({}); + + // Reiniciar estado cuando se abre/cierra el modal + useEffect(() => { + if (abierto && datosIniciales) { + setPedido({ ...datosIniciales }); + setErrores({}); + } else if (!abierto) { + // Limpiar estado cuando se cierra + setPedido({}); + setErrores({}); + } + }, [abierto, datosIniciales]); + + const manejarCambio = useCallback( + (evento) => { + const { name, value } = evento.target; + setPedido((prev) => ({ ...prev, [name]: value })); + + // Limpiar error específico del campo + if (errores[name]) { + setErrores((prev) => ({ ...prev, [name]: false })); + } + }, + [errores] + ); + + const validarCampos = useCallback(() => { + const nuevosErrores = {}; + + // Validar estatus del pedido + if (!pedido.estatusPedido?.trim()) { + nuevosErrores.estatusPedido = true; + } + + // Validar precio total + if (!pedido.precioTotal) { + nuevosErrores.precioTotal = true; + } else { + const valor = pedido.precioTotal.toString().trim(); + const esValido = REGEX_PRECIO.test(valor) && parseFloat(valor) >= 0; + + if (!esValido) { + nuevosErrores.precioTotal = true; + } + } + + // Validar estatus de pago + if (!pedido.estatusPago?.trim()) { + nuevosErrores.estatusPago = true; + } + + // Validar estatus de envío + if (!pedido.estatusEnvio?.trim()) { + nuevosErrores.estatusEnvio = true; + } + + setErrores(nuevosErrores); + return Object.keys(nuevosErrores).length === 0; + }, [pedido]); + + const manejarGuardar = useCallback(async () => { + if (!validarCampos()) { + onMostrarAlerta('Completa todos los campos obligatorios correctamente.', 'error'); + return; + } + + if (!pedido.id) { + onMostrarAlerta('Falta el ID del pedido. No se puede actualizar.', 'error'); + return; + } + + // Mapeo de campos para el backend + const pedidoMapeado = { + idPedido: pedido.id.toString(), + estado: pedido.estatusPedido.trim(), + precioTotal: parseFloat(pedido.precioTotal), + idEnvio: pedido.estatusEnvio.trim(), + idPago: pedido.estatusPago.trim(), + }; + + try { + const resultado = await actualizarPedido(pedidoMapeado); + + if (resultado.exito) { + onMostrarAlerta(resultado.mensaje, 'success'); + onActualizar(); + onCerrar(); + } else { + onMostrarAlerta(resultado.mensaje, 'error'); + } + } catch (error) { + console.error('Error al actualizar pedido:', error); + onMostrarAlerta(error.message || 'Error al actualizar el pedido.', 'error'); + } + }, [pedido, validarCampos, actualizarPedido, onMostrarAlerta, onActualizar, onCerrar]); + + const manejarCambioPrecio = useCallback( + (evento) => { + const value = evento.target.value; + + // Permitir campo vacío + if (value === '') { + manejarCambio(evento); + return; + } + + // Validar formato mientras se escribe + if (REGEX_PRECIO.test(value)) { + manejarCambio(evento); + } + }, + [manejarCambio] + ); + + const manejarCambioEstatus = useCallback( + (evento) => { + const value = evento.target.value; + + // Solo permitir letras, espacios y caracteres especiales del español + if (REGEX_SOLO_LETRAS.test(value)) { + manejarCambio(evento); + } + }, + [manejarCambio] + ); + + const manejarCambioEstatusPago = useCallback( + (evento) => { + const value = evento.target.value; + + // Solo permitir letras, espacios y caracteres especiales del español + if (REGEX_SOLO_LETRAS.test(value)) { + manejarCambio(evento); + } + }, + [manejarCambio] + ); + + const manejarCambioEstatusEnvio = useCallback( + (evento) => { + const value = evento.target.value; + + // Solo permitir letras, espacios y caracteres especiales del español + if (REGEX_SOLO_LETRAS.test(value)) { + manejarCambio(evento); + } + }, + [manejarCambio] + ); + + // Mostrar mensaje de error específico para precio + const obtenerMensajeErrorPrecio = () => { + if (!errores.precioTotal) return ''; + + if (!pedido.precioTotal) { + return 'El precio total es requerido'; + } + + return 'El precio debe tener máximo 8 dígitos enteros y hasta 2 decimales'; + }; + + if (!pedido || !abierto) return null; + + return ( + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ModalEditarPedido; diff --git a/src/Vistas/Componentes/Organismos/ModalFlotante.jsx b/src/Vistas/Componentes/Organismos/ModalFlotante.jsx index d8146eca..c8474c03 100644 --- a/src/Vistas/Componentes/Organismos/ModalFlotante.jsx +++ b/src/Vistas/Componentes/Organismos/ModalFlotante.jsx @@ -71,7 +71,12 @@ const ModalFlotante = ({ }} > {titulo && ( - + {titulo} )} diff --git a/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx b/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx index 90dad3d5..4085273d 100644 --- a/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx +++ b/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx @@ -7,6 +7,7 @@ import Alerta from '@Moleculas/Alerta'; import { tokens } from '@SRC/theme'; import InfoImportar from '@Organismos/InfoImportar'; import CajaDesplazable from '@Organismos/CajaDesplazable'; +import Boton from '@Atomos/Boton'; const ModalImportarEmpleados = ({ abierto, onCerrar, onConfirm, cargando, errores, exito, recargar }) => { const [archivo, setFile] = useState(null); @@ -17,6 +18,7 @@ const ModalImportarEmpleados = ({ abierto, onCerrar, onConfirm, cargando, errore const colores = tokens(theme.palette.mode); const [abririnfo, setAbrirInfo] = useState(false); const [mensajeErrores, setMensajeErrores] = useState(''); + const [descargarCSV, setDescargarCSV] = useState(false); // Manejo de errores en la importación useEffect(() => { @@ -50,6 +52,19 @@ const ModalImportarEmpleados = ({ abierto, onCerrar, onConfirm, cargando, errore } }, [exito, onCerrar, recargar]); + const handleDescargarPlantilla = useCallback(() => { + setDescargarCSV(true); + const link = document.createElement('a'); + link.href = '/plantilla_importar_empleados.csv'; + link.download = 'plantilla_importar_empleados.csv'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + setTimeout(() => { + setDescargarCSV(false); + }, 2000); + }, []); + // Manejo de cierre del modal de importar const handleCerrar = useCallback(() => { onCerrar(false); @@ -159,21 +174,19 @@ const ModalImportarEmpleados = ({ abierto, onCerrar, onConfirm, cargando, errore )} - - Descargar CSV de ejemplo - + setAbrirInfo(false)}> - Consideraciones para tu CSV:
- Campos obligatorios: no dejes celdas vacías en ninguna columna.
- Correo electrónico: formato válido (usuario@dominio.com).
- {`Contraseñas: mínimo 8 caracteres, al menos una mayúscula y un carácter especial (!@#$%^&*(),.?":{}|<>)`}
- Teléfonos: exactamente 10 dígitos, sin espacios ni guiones.
- Textos largos: máximo 75 caracteres por campo.
- Fecha Nacimiento: formato DD/MM/AAAA (p. ej. 05/08/1998).
- Estado: 1 → activo, 0 → inactivo.
- Antiguedad: formato DD/MM/AAAA (p. ej. 05/08/1998).
+ Consideraciones para tu CSV:

+ Campos obligatorios: no dejes celdas vacías en ninguna columna.
+ Correo electrónico: formato válido (usuario@dominio.com).
+ Contraseñas: {`mínimo 8 caracteres, al menos una mayúscula y un carácter especial (!@#$%^&*(),.?":{}|<>)`}
+ Teléfonos: exactamente 10 dígitos, sin espacios ni guiones.
+ Textos largos: máximo 75 caracteres por campo.
+ Fecha Nacimiento: formato DD/MM/AAAA (p. ej. 05/08/1998).
+ Estado: 1 → activo, 0 → inactivo.
+ Antiguedad: formato DD/MM/AAAA (p. ej. 05/08/1998).

{mensajeErrores && ( diff --git a/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx b/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx new file mode 100644 index 00000000..d2bb98ce --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx @@ -0,0 +1,262 @@ +import { useState, useCallback, useEffect } from 'react'; +import { Box, List, ListItem, ListItemIcon, ListItemText, CircularProgress, useTheme } from '@mui/material'; +import Icono from '@Atomos/Icono'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import Alerta from '@Moleculas/Alerta'; +import { tokens } from '@SRC/theme'; +import InfoImportar from '@Organismos/InfoImportar'; +import CajaDesplazable from '@Organismos/CajaDesplazable'; +import ContenedorImportarProductos from './ContenedorImportarProductos'; +import Boton from '../Atomos/Boton'; + +const ModalImportarProductos = ({ abierto, onCerrar, onConfirm, cargando, errores, exito, recargar }) => { + const [archivo, setArchivo] = useState(null); + const [estado, setEstado] = useState('idle'); + const [infoJson, setInfoJson] = useState([]); + const [alerta, setAlerta] = useState(null); + const [abririnfo, setAbrirInfo] = useState(false); + const [mensajeErrores, setMensajeErrores] = useState(''); + const [descargarCSV, setDescargarCSV] = useState(false); + const theme = useTheme(); + const colores = tokens(theme.palette.mode); + + // Manejo de errores en la importación + useEffect(() => { + if (errores && errores.length > 0) { + const mensaje = errores + .map(elemento => `Producto ${elemento.fila}: ${elemento.error}`) + .join('\n'); + setMensajeErrores(mensaje); + } else { + setMensajeErrores(''); + } + }, [errores]); + + // Manejo de éxito en la importación + useEffect(() => { + if (exito) { + setAlerta({ + tipo: 'success', + mensaje: 'Importación completada con éxito.', + icono: true, + cerrable: true, + duracion: 2500, + centradoInferior: true, + }); + recargar(); + setInfoJson([]); + setArchivo(null); + setEstado('idle'); + onCerrar(false); + setMensajeErrores(''); + } + }, [exito, onCerrar, recargar]); + + const handleDescargarPlantilla = useCallback(() => { + setDescargarCSV(true); + const enlace = document.createElement('a'); + enlace.href = '/ejemplo_importar_productos.csv'; + enlace.download = 'ejemplo_importar_productos.csv'; + document.body.appendChild(enlace); + enlace.click(); + document.body.removeChild(enlace); + setTimeout(() => { + setDescargarCSV(false); + }, 2000); + }, []); + + // Manejo de cierre del modal de importar + const handleCerrar = useCallback(() => { + onCerrar(false); + setInfoJson([]); + setArchivo(null); + setEstado('idle'); + setAlerta(null); + setMensajeErrores(''); + }, [onCerrar]); + + // Manejo de archivo aceptados + const handleFileAccepted = (archivo, data) => { + setArchivo(archivo); + setEstado('loading'); + setInfoJson(data); + setEstado('complete'); + }; + + // Manejo de eliminación de archivo + const handleEliminar = () => { + setArchivo(null); + setEstado('idle'); + setInfoJson([]); + }; + + // Manejo de confirmación de importación + const handleConfirmar = () => { + if (!infoJson.length) { + setAlerta({ + tipo: 'error', + mensaje: 'Agrega un archivo CSV con idProducto válidos.', + duracion: 2500, + cerrable: true, + centradoInferior: true, + }); + return; + } + onConfirm(infoJson); + setArchivo(null); + setEstado('idle'); + setAlerta(null); + setInfoJson([]); + + }; + + return ( + <> + + + + setAlerta({ + tipo: 'error', + mensaje, + duracion: 3000, + cerrable: true, + centradoInferior: true, + }) + } + /> + + {archivo && ( + + + } + > + + {estado === 'Cargando...' ? ( + + ) : ( + + )} + + + + + )} + + + + setAbrirInfo(false)}> + <> + ¿Cómo funciona la estructura del archivo CSV? +

+ Cada fila del archivo representa una opción específica de un producto. El sistema necesita saber qué filas pertenecen al mismo producto. Para eso sirve el campo idProducto. +

+ Dentro de ese producto, se organizan las variantes usando el campo nombreVariante. + Y dentro de cada variante, se agrupan las opciones como "Rojo", "M", "32GB", etc. Consulta el archivo de ejemplo. +

+ Así se puede importar correctamente todo: producto, variantes y sus opciones. +

+ + Asegúrate de lo sigiente: +
    +
  • + El idProducto sea único para cada producto dentro del archivo. Puede ser cualquier número, pero debe coincidir en todas las filas relacionadas con ese producto. Por cada campo vacío de idProducto, el sistema ignorará la fila. +
  • +
  • + La primera fila de cada producto no debe tener campos vacíos, ya que el sistema la usa para identificar el producto principal. +
  • +
  • + El csv tenga formato UTF-8 para evitar problemas con caracteres especiales. +
  • +
  • + El idProveedor exista en el sistema. El campo puede estar vacío si no se usa. +
  • +
+ +

Producto

+ idProducto permite al sistema saber qué filas pertenecen al mismo producto, aunque estén en diferentes líneas del CSV. Es necesario para poder agruparlo.
+ idProveedor (opcional): Identificador del proveedor del producto. Puede ser un campo vacío
+ nombreProducto, nombreComercial: Nombres básicos
+ descripcionProducto: Descripción del producto
+ marca, modelo, tipoProducto: Datos básicos del producto
+ costo, precioVenta, precioCliente: Valores numéricos (usa punto decimal)
+ precioPuntos: Número entero
+ impuesto, descuento: Valor numérico del porcentaje. Ejemplo: 16 (para 16%)
+ estado: 1 = activo, 0 = inactivo
+ envio: 1 = disponible, 0 = no disponible

+

Variante

+ nombreVariante: Ej. "Color", "Tamaño"
+ descripcionVariante: Descripción de la variante

+

Opción

+ valorOpcion: Ej. "Rojo", "XL"
+ SKUautomatico: Obligatorio
+ SKUcomercial: Código visible al cliente
+ cantidad: Entero
+ costoAdicional: Número positivo
+ descuentoOpcion: Valor numérico del porcentaje. Ejemplo: 50 (para 50%)
+ estadoOpcion: 1 = activa, 0 = inactiva

+ + +
+

+ {mensajeErrores && ( + + + + {mensajeErrores} + + + + )} +
+ + + {alerta && ( + setAlerta(null)} + /> + )} + + + ); +}; + +export default ModalImportarProductos; diff --git a/src/Vistas/Componentes/Organismos/ModalUsuarios.jsx b/src/Vistas/Componentes/Organismos/ModalUsuarios.jsx new file mode 100644 index 00000000..ca4debf6 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalUsuarios.jsx @@ -0,0 +1,84 @@ +import { Box } from '@mui/material'; +import Alerta from '@Moleculas/Alerta'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import FormularioActualizarUsuario from './Formularios/FormularioActualizarUsuario'; +import { useAccionesUsuario } from '@Hooks/Usuarios/useAccionesUsuario'; + +const ModalUsuarios = ({ open, onClose, onAccion, usuarioEdicion }) => { + const { + datosUsuario, + erroresValidacion, + alerta, + setAlerta, + esEdicion, + manejarCambio, + manejarFechaNacimiento, + obtenerHelperText, + handleGuardar, + CAMPO_OBLIGATORIO, + roles, + clientes, + esSuperAdmin, + cargandoRoles, + } = useAccionesUsuario(usuarioEdicion); + + const manejarConfirmacion = async () => { + const resultado = await handleGuardar(); + if (resultado?.exito) { + if (onAccion) await onAccion(); + setTimeout(() => { + onClose(); + }, 1500); + } + }; + + const manejarCierre = () => { + setAlerta(null); + onClose(); + }; + + return ( + + + + + {alerta && ( + setAlerta(null)} + /> + )} + + ); +}; + +export default ModalUsuarios; diff --git a/src/Vistas/Componentes/Organismos/TablaSetsEmpleados.jsx b/src/Vistas/Componentes/Organismos/TablaSetsEmpleados.jsx new file mode 100644 index 00000000..717b7c4b --- /dev/null +++ b/src/Vistas/Componentes/Organismos/TablaSetsEmpleados.jsx @@ -0,0 +1,25 @@ +import Tabla from '@Organismos/Tabla'; + +const TablaSetsEmpleados = ({ + columnas, + filas, + paginacion, + checkBox, + onRowClick, + onRowSelectionModelChange, + selectionModel, +}) => { + return ( + + ); +}; + +export default TablaSetsEmpleados; diff --git a/src/Vistas/Paginas/Categorias/ListaCategorias.jsx b/src/Vistas/Paginas/Categorias/ListaCategorias.jsx index 2066b081..8d4da7b1 100644 --- a/src/Vistas/Paginas/Categorias/ListaCategorias.jsx +++ b/src/Vistas/Paginas/Categorias/ListaCategorias.jsx @@ -1,69 +1,92 @@ -import React, { useState } from 'react'; +// RF49 - Actualizar Categoría - ListaCategorias.jsx + +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import Tabla from '@Organismos/Tabla'; import Alerta from '@Moleculas/Alerta'; import ContenedorLista from '@Organismos/ContenedorLista'; import ModalEliminarCategoria from '@Organismos/ModalEliminarCategoria'; +import CategoriaInfo from '@Organismos/CategoriaInfo'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import ModalEditarCategoria from '@Organismos/ModalEditarCategoria'; import { useConsultarCategorias } from '@Hooks/Categorias/useConsultarCategorias'; +import { leerCategoria } from '@Hooks/Categorias/useLeerCategoria'; +import useActualizarCategoria from '@Hooks/Categorias/useActualizarCategoria'; +import obtenerProductosCategoria from '@Servicios/obtenerProductosCategoria'; +import { PERMISOS } from '@Utilidades/Constantes/permisos'; +import { useAuth } from '@Hooks/AuthProvider'; import { Box, useTheme } from '@mui/material'; import { tokens } from '@SRC/theme'; import ModalCrearCategoria from '@Organismos/ModalCrearCategoria'; -/** - * Página para consultar y mostrar la lista de categorías en una tabla. - * - * Muestra los resultados en un CustomDataGrid, incluyendo - * nombre, descripción y número de productos de cada categoría. - * - * @see [RF[47] Consulta lista de categorías](https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF47) - */ - const ListaCategorias = () => { const { categorias, cargando, error, recargar } = useConsultarCategorias(); + const [productos, setProductos] = useState([]); const [seleccionados, setSeleccionados] = useState(new Set()); const [alerta, setAlerta] = useState(null); const [idsCategoria, setIdsCategoria] = useState([]); + const [modalCrearAbierto, setModalCrearAbierto] = useState(false); + const [openModalEliminar, setOpenModalEliminar] = useState(false); + const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); + const [categoriaDetalle, setCategoriaDetalle] = useState(null); + const [errorDetalle, setErrorDetalle] = useState(false); + const [cargandoDetalle, setCargandoDetalle] = useState(false); + const [modalEditarAbierto, setModalEditarAbierto] = useState(false); + const [categoriaEditable, setCategoriaEditable] = useState(null); + + const actualizar = useActualizarCategoria(); const theme = useTheme(); const colores = tokens(theme.palette.mode); + const { usuario } = useAuth(); + - // Estado para controlar la visualización del modal crear - const [modalCrearAbierto, setModalCrearAbierto] = useState(false); + const columns = useMemo( + () => [ + { field: 'nombreCategoria', headerName: 'Nombre', flex: 1 }, + { field: 'descripcion', headerName: 'Descripción', flex: 2 }, + { + field: 'cantidadProductos', + headerName: 'Número de productos asociados', + type: 'number', + flex: 1, + }, + ], + [] + ); - // Estado para controlar la visualización del modal eliminar - const [openModalEliminar, setOpenModalEliminar] = useState(false); + const rows = useMemo( + () => + categorias.map((cat) => ({ + id: cat.idCategoria, + nombreCategoria: cat.nombreCategoria, + descripcion: cat.descripcion, + cantidadProductos: cat.cantidadProductos, + idCliente: cat.idCliente, + })), + [categorias] + ); - // Columnas para el DataGrid - const columns = [ - { field: 'nombreCategoria', headerName: 'Nombre', flex: 1 }, - { field: 'descripcion', headerName: 'Descripción', flex: 2 }, - { - field: 'cantidadProductos', - headerName: 'Número de productos asociados', - type: 'number', - flex: 1, - }, - ]; - - // Las filas deben tener un campo `id`, usamos `idCategoria` - const rows = categorias.map((cat) => ({ - id: cat.idCategoria, - nombreCategoria: cat.nombreCategoria, - descripcion: cat.descripcion, - cantidadProductos: cat.cantidadProductos, - idCliente: cat.idCliente, - })); - - // Manejador para abrir el modal - const handleAbrirModalCrear = () => { - setModalCrearAbierto(true); - }; - - // Manejador para cerrar el modal - const handleCerrarModalCrear = () => { - setModalCrearAbierto(false); - }; - - // Manejador para cuando se crea una nueva categoría - const handleCategoriaCreadaExitosamente = () => { + useEffect(() => { + const cargarProductos = async () => { + try { + const resultado = await obtenerProductosCategoria(); + setProductos(resultado); + } catch { + setAlerta({ + tipo: 'error', + mensaje: 'Error al cargar productos.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + } + }; + cargarProductos(); + }, []); + + const handleAbrirModalCrear = useCallback(() => setModalCrearAbierto(true), []); + const handleCerrarModalCrear = useCallback(() => setModalCrearAbierto(false), []); + + const handleCategoriaCreadaExitosamente = useCallback(() => { handleCerrarModalCrear(); // Recarga la lista de categorías recargar(); @@ -75,10 +98,10 @@ const ListaCategorias = () => { cerrable: true, centradoInferior: true, }); - }; + }, [recargar, handleCerrarModalCrear]); // Manejador para errores al crear categoría - const handleErrorCrearCategoria = (mensajeError) => { + const handleErrorCrearCategoria = useCallback((mensajeError) => { setAlerta({ tipo: 'error', mensaje: mensajeError, @@ -86,38 +109,183 @@ const ListaCategorias = () => { cerrable: true, centradoInferior: true, }); - }; - - const botones = [ - { - label: 'Añadir', - variant: 'contained', - color: 'error', - size: 'large', - backgroundColor: colores.altertex[1], - onClick: handleAbrirModalCrear, - }, - { - label: 'Eliminar', - onClick: () => { - if (seleccionados.size === 0 || seleccionados.ids.size === 0) { - setAlerta({ - tipo: 'error', - mensaje: 'Selecciona al menos una categoría para eliminar.', - icono: true, - cerrable: true, - centradoInferior: true, - }); - } else { - setIdsCategoria(Array.from(seleccionados.ids)); - setOpenModalEliminar(true); - } + }, []); + + const mostrarDetalleCategoria = useCallback(async (idCategoria) => { + setCargandoDetalle(true); + setCategoriaDetalle(null); + setErrorDetalle(false); + + try { + const detalle = await leerCategoria(idCategoria); + detalle.idCategoria = idCategoria; + setCategoriaDetalle(detalle); + } catch { + setErrorDetalle(true); + setCategoriaDetalle({ nombreCategoria: '', descripcion: '', productos: [] }); + setAlerta({ + tipo: 'error', + mensaje: 'Error al obtener los datos de la categoría.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + } finally { + setCargandoDetalle(false); + setModalDetalleAbierto(true); + } + }, []); + + const manejarCambioTransferencia = useCallback(({ disponibles, seleccionados }) => { + setCategoriaEditable((prev) => ({ + ...prev, + productosDisponibles: disponibles, + productosSeleccionados: seleccionados, + })); + }, []); + + const manejarGuardarCategoria = useCallback(async () => { + if (!categoriaEditable?.idCategoria) { + setAlerta({ + tipo: 'error', + mensaje: 'No se pudo determinar el ID de la categoría.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + return; + } + + const datos = { + nombreCategoria: categoriaEditable.nombreCategoria, + descripcion: categoriaEditable.descripcion, + productos: categoriaEditable.productosSeleccionados.map((producto) => producto.id), + }; + + const resultado = await actualizar.actualizarCategoria(categoriaEditable.idCategoria, datos); + + if (resultado.success) { + setAlerta({ + tipo: 'success', + mensaje: actualizar.mensaje || 'Categoría actualizada correctamente', + icono: true, + cerrable: true, + centradoInferior: true, + }); + + setTimeout(() => { + setModalEditarAbierto(false); + setCategoriaEditable(null); + actualizar.limpiarEstado(); + setAlerta(null); + recargar(); + }, 2000); + } else if (actualizar.error) { + setAlerta({ + tipo: 'error', + mensaje: actualizar.error, + icono: true, + cerrable: true, + centradoInferior: true, + }); + } + }, [categoriaEditable, actualizar, recargar]); + + const cerrarModalEditar = useCallback(() => { + setModalEditarAbierto(false); + setCategoriaEditable(null); + actualizar.limpiarEstado(); + }, [actualizar]); + + const abrirModalEditar = useCallback(async () => { + setModalDetalleAbierto(false); + + const productosAsociados = productos.filter((producto) => + categoriaDetalle.productos.includes(producto.nombre)); + + const productosDisponibles = productos.filter( + (producto) => !categoriaDetalle.productos.includes(producto.nombre) + ); + + const nuevaCategoriaEditable = { + idCategoria: categoriaDetalle.idCategoria, + nombreCategoria: categoriaDetalle.nombreCategoria, + descripcion: categoriaDetalle.descripcion, + productosSeleccionados: productosAsociados, + productosDisponibles, + }; + + setCategoriaEditable(nuevaCategoriaEditable); + setModalEditarAbierto(true); + }, [productos, categoriaDetalle]); + + const botones = useMemo( + () => [ + { + label: 'Añadir', + variant: 'contained', + color: 'error', + size: 'large', + backgroundColor: colores.altertex[1], + onClick: handleAbrirModalCrear, + disabled: !usuario?.permisos?.includes(PERMISOS.CREAR_CATEGORIA_PRODUCTOS), + }, + { + label: 'Eliminar', + disabled: !usuario?.permisos?.includes(PERMISOS.ELIMINAR_CATEGORIA_PRODUCTOS), + onClick: () => { + if (seleccionados.size === 0 || seleccionados.ids?.size === 0) { + setAlerta({ + tipo: 'error', + mensaje: 'Selecciona al menos una categoría para eliminar.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + } else { + setIdsCategoria(Array.from(seleccionados.ids)); + setOpenModalEliminar(true); + } + }, + color: 'error', + size: 'large', + backgroundColor: colores.altertex[1], }, - color: 'error', - size: 'large', - backgroundColor: colores.altertex[1], - }, - ]; + + ], + [colores.altertex, handleAbrirModalCrear, seleccionados, usuario?.permisos] + ); + + const botonesModalDetalle = useMemo(() => { + if (errorDetalle) { + return [ + { + label: 'SALIR', + variant: 'outlined', + color: 'primary', + outlineColor: colores.altertex[1], + onClick: () => setModalDetalleAbierto(false), + }, + ]; + } + + return [ + { + label: 'EDITAR', + variant: 'contained', + color: 'error', + backgroundColor: colores.altertex[1], + onClick: abrirModalEditar, + }, + { + label: 'SALIR', + variant: 'outlined', + color: 'primary', + outlineColor: colores.altertex[1], + onClick: () => setModalDetalleAbierto(false), + }, + ]; + }, [errorDetalle, colores.altertex, abrirModalEditar]); return ( <> @@ -132,11 +300,10 @@ const ListaCategorias = () => { columns={columns} rows={rows} loading={cargando} - disableRowSelectionOnClick={true} + disableRowSelectionOnClick checkboxSelection - onRowSelectionModelChange={(newSelection) => { - setSeleccionados(newSelection); - }} + onRowSelectionModelChange={(newSelection) => setSeleccionados(newSelection)} + onRowClick={(params) => mostrarDetalleCategoria(params.row.id)} /> @@ -158,7 +325,43 @@ const ListaCategorias = () => { refrescarPagina={recargar} /> - {/* Alert that appears on the page level */} + {modalDetalleAbierto && !cargandoDetalle && ( + { + setModalDetalleAbierto(false); + setCategoriaDetalle(null); + setErrorDetalle(false); + }} + onConfirm={() => setModalDetalleAbierto(false)} + titulo={ + errorDetalle + ? 'Cargando...' + : categoriaDetalle?.nombreCategoria || 'Detalles de la categoría' + } + tituloVariant='h4' + botones={botonesModalDetalle} + > + {!errorDetalle && ( + + )} + + )} + + + {alerta && ( { )} ); -}; +}; // <- Esta llave de cierre estaba faltando -export default ListaCategorias; \ No newline at end of file +export default ListaCategorias; diff --git a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx index 54423435..054bd936 100644 --- a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx +++ b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx @@ -1,17 +1,26 @@ +// RF[32] - Consulta Lista de Cuotas - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF32 +//RF[31] Consulta crear set de cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF31] +//RF[33] Leer cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF33] + import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTheme, Box } from '@mui/material'; import { tokens } from '@SRC/theme'; -import { useConsultarCuotas } from '@Hooks/Cuotas/useConsultarCuotas'; import { useAuth } from '@Hooks/AuthProvider'; import ContenedorLista from '@Organismos/ContenedorLista'; import Tabla from '@Organismos/Tabla'; import Chip from '@Atomos/Chip'; import ModalCrearCuotaSet from '@Organismos/Cuotas/ModalCrearCuotaSet'; +import ModalEditarCuotas from '@Organismos/Cuotas/ModalEditarCuotas'; import Alerta from '@Moleculas/Alerta'; +import { useCuotaId } from '@Hooks/Cuotas/useLeerCuota'; import PopUpEliminar from '@Moleculas/PopUp'; import { RUTAS } from '@Constantes/rutas'; import { RepositorioEliminarSetCuotas } from '@Dominio/Repositorios/Cuotas/repositorioEliminarSetCuotas'; +import { PERMISOS } from '@Utilidades/Constantes/permisos'; +import { useConsultarCuotas } from '@Hooks/Cuotas/useConsultarCuotas'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import CuotasInfo from '@Moleculas/CuotasInfo'; const ListaCuotas = () => { const navegar = useNavigate(); @@ -25,6 +34,16 @@ const ListaCuotas = () => { const [idsSetCuotas, setIdsSetCuotas] = useState([]); const [alerta, setAlerta] = useState(null); const [abrirPopUpEliminar, setAbrirPopUpEliminar] = useState(false); + const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); + const [modalEditarAbierto, setModalEditarAbierto] = useState(false); + const [idSetCuotaSeleccionado, setIdSetCuotaSeleccionado] = useState(null); + + const { + cuota, + cargando: cargandoDetalle, + error: errorDetalle, + } = useCuotaId(modalDetalleAbierto || modalEditarAbierto ? idSetCuotaSeleccionado : null); + useEffect(() => { if (!usuario?.clienteSeleccionado) { @@ -66,11 +85,14 @@ const ListaCuotas = () => { const filas = Array.isArray(cuotas) ? cuotas.map((cuota) => ({ - id: cuota.idCuotaSet, - nombre: cuota.nombre, - periodoRenovacion: cuota.periodoRenovacion, - renovacionHabilitada: cuota.renovacionHabilitada === 1, - })) + id: cuota.idCuotaSet, + idCuotaSet: cuota.idCuotaSet, + nombre: cuota.nombre, + periodoRenovacion: cuota.periodoRenovacion, + renovacionHabilitada: cuota.renovacionHabilitada === 1, + descripcion: cuota.descripcion, + ultimaActualizacion: cuota.ultimaActualizacion, + })) : []; const handleAbrirModalCrear = () => setModalCrearAbierto(true); @@ -102,6 +124,28 @@ const ListaCuotas = () => { } }; + const handleAbrirEditar = () => { + setModalDetalleAbierto(false); + setModalEditarAbierto(true); + }; + + const handleCerrarEditar = () => { + setModalEditarAbierto(false); + }; + + const handleCuotaActualizada = async () => { + await recargar(); + setModalEditarAbierto(false); + setIdSetCuotaSeleccionado(null); + setAlerta({ + tipo: 'success', + mensaje: 'Set de cuotas actualizado correctamente.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + }; + const botones = [ { label: 'Añadir', @@ -110,9 +154,11 @@ const ListaCuotas = () => { size: 'large', backgroundColor: colores.altertex[1], onClick: handleAbrirModalCrear, + disabled: !usuario?.permisos?.includes(PERMISOS.CREAR_SET_CUOTAS), }, { label: 'Eliminar', + disabled: !usuario?.permisos?.includes(PERMISOS.ELIMINAR_SET_CUOTAS), onClick: () => { if (seleccionados.length === 0) { setAlerta({ @@ -162,6 +208,10 @@ const ListaCuotas = () => { const ids = Array.isArray(nuevosIds) ? nuevosIds : Array.from(nuevosIds?.ids || []); setSeleccionados(ids); }} + onRowClick={(params) => { + setIdSetCuotaSeleccionado(params.row.idCuotaSet); + setModalDetalleAbierto(true); + }} /> @@ -179,6 +229,52 @@ const ListaCuotas = () => { dialogo='¿Estás seguro de que deseas eliminar los sets de cuotas seleccionados? Esta acción no se puede deshacer.' /> + {/* MODAL DE DETALLE */} + {modalDetalleAbierto && ( + setModalDetalleAbierto(false)} + titulo={cuota?.nombre || 'Cargando...'} + tituloVariant='h4' + customWidth={530} + botones={[ + ...(usuario?.permisos?.includes(PERMISOS.ACTUALIZAR_SET_CUOTAS) ? [{ + label: 'EDITAR', + variant: 'contained', + color: 'error', + backgroundColor: colores.altertex[1], + onClick: handleAbrirEditar, + }] : []), + { + label: 'Salir', + variant: 'outlined', + color: 'primary', + outlineColor: colores.primario[1], + onClick: () => setModalDetalleAbierto(false), + style: { marginTop: '10px' }, + }, + ]} + > + {cargandoDetalle ? ( +

Cargando información del set de cuotas...

+ ) : errorDetalle ? ( +

Error al cargar la información del set de cuotas: {errorDetalle}

+ ) : ( + + )} +
+ )} + + {/* MODAL DE EDITAR */} + + {alerta && ( { const { empleados, cargando, error, recargar } = useConsultarEmpleados(); @@ -45,7 +49,7 @@ const ListaGrupoEmpleados = () => { const manejarConfirmarEliminar = async () => { try { await eliminar(empleadosSeleccionados); - await recargar(); // Se asegura de que se recargue la lista + await recargar(); setAlerta({ tipo: 'success', mensaje: 'Empleados eliminados correctamente.', @@ -67,6 +71,68 @@ const ListaGrupoEmpleados = () => { } }; + const [openModalExportar, setAbrirPopUpExportar] = useState(false); + const MENSAJE_POPUP_EXPORTAR = '¿Deseas exportar la lista de empleados? El archivo será generado en formato CSV.'; + const manejarCancelarExportar = () => { + setAbrirPopUpExportar(false); + }; + + const manejarConfirmarExportar = async () => { + if (empleadosSeleccionados.length === 0) { + setAlerta({ + tipo: 'warning', + mensaje: 'Selecciona al menos un empleado para exportar.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + return; + } + + await exportar(empleadosSeleccionados); + setAbrirPopUpExportar(false); + }; + + const { exportar, error: errorExportar, csv, mensaje } = useExportarEmpleados(); + + useEffect(() => { + if (csv) { + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', 'empleados.csv'); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } + }, [csv]); + + useEffect(() => { + if (errorExportar) { + setAlerta({ + tipo: 'error', + mensaje: errorExportar, + icono: true, + cerrable: true, + centradoInferior: true, + }); + } + }, [errorExportar]); + + useEffect(() => { + if (csv && mensaje) { + setAlerta({ + tipo: 'success', + mensaje, + icono: true, + cerrable: true, + centradoInferior: true, + }); + } + }, [csv, mensaje]); + const columnas = [ { field: 'nombreCompleto', headerName: 'Nombre del Empleado', flex: 1 }, { field: 'correoElectronico', headerName: 'Correo Electrónico', flex: 1 }, @@ -97,7 +163,7 @@ const ListaGrupoEmpleados = () => { color: 'error', size: 'large', backgroundColor: colores.altertex[1], - construccion: true, + disabled: !usuario?.permisos?.includes(PERMISOS.CREAR_EMPLEADO), }, { variant: 'outlined', @@ -109,13 +175,13 @@ const ListaGrupoEmpleados = () => { disabled: !usuario?.permisos?.includes(PERMISOS.IMPORTAR_EMPLEADOS), }, { - //variant: 'outlined', + variant: 'outlined', label: 'Exportar', - onClick: () => console.log('Exportar'), + onClick: () => setAbrirPopUpExportar(true), color: 'primary', + outlineColor: colores.altertex[1], size: 'large', - //outlineColor: colores.primario[10], - construccion: true, + disabled: !usuario?.permisos?.includes(PERMISOS.EXPORTAR_EMPLEADOS), }, { label: 'Eliminar', @@ -159,7 +225,8 @@ const ListaGrupoEmpleados = () => { setModalDetalleAbierto(true); }} onRowSelectionModelChange={(nuevosIds) => { - const ids = Array.isArray(nuevosIds) ? nuevosIds : Array.from(nuevosIds?.ids || []); + const ids = (Array.isArray(nuevosIds) ? nuevosIds : Array.from(nuevosIds?.ids || [])) + .map(id => parseInt(id)); setEmpleadosSeleccionados(ids); }} /> @@ -214,18 +281,10 @@ const ListaGrupoEmpleados = () => { /> )} - {/* Modal para agregar empleado */} - {modalAgregarAbierto && ( - - )} {/* Modal para actualizar empleado */} {modalActualizarAbierto && ( - setModalActualizarAbierto(false)} onAccion={recargar} @@ -233,6 +292,15 @@ const ListaGrupoEmpleados = () => { /> )} + {/* Modal para agregar empleado */} + {modalAgregarAbierto && ( + + )} + {/* PopUp de confirmación para eliminar */} { dialogo={MENSAJE_POPUP_ELIMINAR} /> + + {/* Alerta inferior */} {alerta && ( { ); }; -export default ListaGrupoEmpleados; +export default ListaGrupoEmpleados; \ No newline at end of file diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index ac72bbe4..6f4d3abf 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -1,3 +1,5 @@ +// RF22 - Consulta Lista de Grupo Empleados - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF22 +// RF23 Lee grupo de empleados -https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF23 import React, { useState } from 'react'; import Tabla from '@Organismos/Tabla'; import ContenedorLista from '@Organismos/ContenedorLista'; @@ -13,6 +15,8 @@ import InfoGrupoEmpleados from '@Moleculas/GrupoEmpleadosInfo'; import { useGrupoEmpleadosId } from '@Hooks/Empleados/useLeerGrupoEmpleados'; import ModalFlotante from '@Organismos/ModalFlotante'; import ModalCrearGrupoEmpleado from '@Organismos/ModalCrearGrupoEmpleado'; +import InfoGrupoEmpleadosEditable from '@Moleculas/GrupoEmpleadosInfoEditable'; +import { useActualizarGrupoEmpleados } from '@Hooks/Empleados/useActualizarGrupoEmpleados'; const ListaGrupoEmpleados = () => { const { grupos, cargando, error, refetch } = useConsultarGrupos(); @@ -29,6 +33,8 @@ const ListaGrupoEmpleados = () => { const [abrirPopUpEliminar, setAbrirPopUpEliminar] = useState(false); const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); const [idGrupoSeleccionado, setIdGrupoSeleccionado] = useState(null); + const [abrirModalEditar, setAbrirModalEditar] = useState(false); + const [formData, setFormData] = useState(null); const { grupoEmpleados, @@ -69,6 +75,52 @@ const ListaGrupoEmpleados = () => { } }; + const handleFormDataChange = (data) => { + setFormData(data); + }; + + const { actualizarGrupo } = useActualizarGrupoEmpleados(); + + const handleGuardar = async () => { + if (!formData?.esValido()) { + setAlerta({ + tipo: 'error', + mensaje: 'El nombre y la descripción son obligatorios.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + return; + } + + try { + await actualizarGrupo( + idGrupoSeleccionado, + formData.nombre, + formData.descripcion, + formData.empleados, + formData.setsDeProductos + ); + await refetch(); + setAbrirModalEditar(false); + setAlerta({ + tipo: 'success', + mensaje: 'Grupo de empleados actualizado correctamente.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + } catch (error) { + setAlerta({ + tipo: 'error', + mensaje: error?.message || 'Error al actualizar el grupo de empleados.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + } + }; + const columnas = [ { field: 'nombre', @@ -109,6 +161,8 @@ const ListaGrupoEmpleados = () => { color: 'error', size: 'large', backgroundColor: colores.altertex[1], + disabled: !usuario?.permisos?.includes(PERMISOS.CREAR_GRUPO_EMPLEADOS), + // construccion: true, }, { label: 'Eliminar', @@ -210,9 +264,11 @@ const ListaGrupoEmpleados = () => { label: 'Editar', variant: 'contained', color: 'primary', - backgroundColor: colores.altertex[1], - onClick: () => console.log('Editar usuario'), - construccion: true, + outlineColor: colores.primario[10], + onClick: () => { + setModalDetalleAbierto(false); + setAbrirModalEditar(true); + }, }, { label: 'Salir', @@ -238,6 +294,47 @@ const ListaGrupoEmpleados = () => { )} )} + {abrirModalEditar && ( + setAbrirModalEditar(false)} + titulo='Editar Grupo de Empleados' + tituloVariant='h4' + customWidth={900} + botones={[ + { + label: 'Guardar', + variant: 'contained', + color: 'primary', + outlineColor: colores.primario[10], + onClick: handleGuardar, + }, + { + label: 'Cancelar', + variant: 'outlined', + color: 'primary', + outlineColor: colores.primario[10], + onClick: () => setAbrirModalEditar(false), + }, + ]} + > + {cargandoDetalle ? ( +

Cargando información del grupo de empleados...

+ ) : errorDetalle ? ( +

Error al cargar la información del grupo de empleados: {errorDetalle}

+ ) : ( + + )} +
+ )} ); }; diff --git a/src/Vistas/Paginas/Eventos/ListaEventos.jsx b/src/Vistas/Paginas/Eventos/ListaEventos.jsx index afc612f4..8429fe11 100644 --- a/src/Vistas/Paginas/Eventos/ListaEventos.jsx +++ b/src/Vistas/Paginas/Eventos/ListaEventos.jsx @@ -1,5 +1,6 @@ +// RF36 - Crear Evento - [https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF36] // RF37 - Consulta Lista de Eventos - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF37 -//RF38 - Leer Evento - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF38 +// RF38 - Leer Evento - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF38 import React, { useState } from 'react'; import Tabla from '@Organismos/Tabla'; @@ -8,6 +9,7 @@ import Alerta from '@Moleculas/Alerta'; import PopUp from '@Moleculas/PopUp'; import { Box, useTheme } from '@mui/material'; import ModalFlotante from '@Organismos/ModalFlotante'; +import ModalCrearEvento from '@Organismos/Eventos/ModalCrearEvento'; import { useEventoId } from '@Hooks/Eventos/useLeerEvento'; import { useConsultarEventos } from '@Hooks/Eventos/useConsultarEventos'; import InfoEvento from '@Moleculas/EventoInfo'; @@ -15,25 +17,60 @@ import { tokens } from '@SRC/theme'; import { PERMISOS } from '@Utilidades/Constantes/permisos'; import { useAuth } from '@Hooks/AuthProvider'; import { useEliminarEvento } from '@Hooks/Eventos/useEliminarEvento'; +import { useCrearEvento } from '@SRC/hooks/Eventos/useCrearEvento'; const ListaEventos = () => { const { eventos, cargando, error, recargar } = useConsultarEventos(); + const { crear } = useCrearEvento(); + const { eliminar } = useEliminarEvento(); + const { usuario } = useAuth(); + const theme = useTheme(); const colores = tokens(theme.palette.mode); + const MENSAJE_POPUP_ELIMINAR = '¿Estás seguro de que deseas eliminar los eventos seleccionados?'; const [seleccionados, setSeleccionados] = useState([]); const [alerta, setAlerta] = useState(null); - const { eliminar } = useEliminarEvento(); - const [abrirEliminar, setAbrirPopUpEliminar] = useState(false); - const { usuario } = useAuth(); + const [abrirCrear, setAbrirCrear] = useState(false); + const [abrirEliminar, setAbrirEliminar] = useState(false); const [eventoSeleccionado, setEventoSeleccionado] = useState(null); const [modalAbierto, setModalAbierto] = useState(false); const { evento } = useEventoId(eventoSeleccionado ? eventoSeleccionado.id : null); + const manejarAbrirCrear = () => { + setAbrirCrear(true); + }; + const manejarCancelarCrear = () => { + setAbrirCrear(false); + }; + + const manejarConfirmarCrear = async (evento) => { + try { + await crear(evento); + if (typeof recargar === 'function') await recargar(); + setAlerta({ + tipo: 'success', + mensaje: 'Evento creado correctamente.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + setAbrirCrear(false); + } catch (error) { + setAlerta({ + tipo: 'error', + mensaje: `Error al crear el evento: ${error.message || 'Ocurrió un error desconocido'}`, + icono: true, + cerrable: true, + centradoInferior: true, + }); + } + }; + const manejarCancelarEliminar = () => { - setAbrirPopUpEliminar(false); + setAbrirEliminar(false); }; const manejarConfirmarEliminar = async () => { @@ -57,7 +94,7 @@ const ListaEventos = () => { centradoInferior: true, }); } finally { - setAbrirPopUpEliminar(false); + setAbrirEliminar(false); } }; @@ -65,8 +102,6 @@ const ListaEventos = () => { { field: 'nombre', headerName: 'Nombre', flex: 1 }, { field: 'descripcion', headerName: 'Descripción', flex: 2 }, { field: 'puntos', headerName: 'Puntos', flex: 1 }, - { field: 'periodo', headerName: 'Periodo', flex: 1 }, - { field: 'renovacion', headerName: 'Renovación', flex: 1 }, ]; const filas = (eventos || []).map((evento) => ({ @@ -85,8 +120,8 @@ const ListaEventos = () => { color: 'error', size: 'large', backgroundColor: colores.altertex[1], - onClick: () => console.log('Añadir'), - construccion: true, + onClick: manejarAbrirCrear, + disabled: !usuario?.permisos?.includes(PERMISOS.CREAR_EVENTO), }, { label: 'Eliminar', @@ -100,7 +135,7 @@ const ListaEventos = () => { centradoInferior: true, }); } else { - setAbrirPopUpEliminar(true); + setAbrirEliminar(true); } }, disabled: !usuario?.permisos?.includes(PERMISOS.ELIMINAR_EVENTO), @@ -197,6 +232,11 @@ const ListaEventos = () => { confirmar={manejarConfirmarEliminar} dialogo={MENSAJE_POPUP_ELIMINAR} /> + ); }; diff --git a/src/Vistas/Paginas/Pedidos/ListaPedidos.jsx b/src/Vistas/Paginas/Pedidos/ListaPedidos.jsx index ae3aca97..6fcf5d32 100644 --- a/src/Vistas/Paginas/Pedidos/ListaPedidos.jsx +++ b/src/Vistas/Paginas/Pedidos/ListaPedidos.jsx @@ -7,6 +7,8 @@ import ContenedorLista from '@Organismos/ContenedorLista'; import Tabla from '@Organismos/Tabla'; import Alerta from '@Moleculas/Alerta'; import PopUp from '@Moleculas/PopUp'; +import Boton from '@Atomos/Boton'; +import ModalEditarPedido from '@Organismos/ModalEditarPedido'; import { tokens } from '@SRC/theme'; import { useConsultarPedidos } from '@Hooks/Pedidos/useConsultarPedidos'; import { useEliminarPedido } from '@Hooks/Pedidos/useEliminarPedido'; @@ -23,12 +25,16 @@ const ListaPedidos = () => { const [alerta, setAlerta] = useState(null); const { eliminar } = useEliminarPedido(); - // Estado para controlar la visualización del modal eliminar - const [abrirPopUpEliminar, setAbrirPopUpEliminar] = useState(false); + const { usuario } = useAuth(); + + // Estados para actualizar pedido + const [modalAbierto, setModalAbierto] = useState(false); + const [pedidoSeleccionado, setPedidoSeleccionado] = useState(null); + const manejarCancelarEliminar = () => { setAbrirPopUpEliminar(false); }; - const { usuario } = useAuth(); + const manejarConfirmarEliminar = async () => { try { await eliminar(seleccionados); @@ -54,42 +60,40 @@ const ListaPedidos = () => { } }; + const [abrirPopUpEliminar, setAbrirPopUpEliminar] = useState(false); + + // Funciones para editar pedido + const abrirModalEditar = (pedido) => { + console.log('Fila seleccionada:', pedido); + setPedidoSeleccionado(pedido); + setModalAbierto(true); + }; + + const cerrarModal = () => { + setPedidoSeleccionado(null); + setModalAbierto(false); + }; + + const mostrarAlerta = (mensaje, tipo) => { + setAlerta({ + tipo, + mensaje, + icono: true, + cerrable: true, + centradoInferior: true, + }); + setTimeout(() => setAlerta(null), 3000); + }; + const columnas = [ - { - field: 'pedido', - headerName: 'Pedido ID', - flex: 1, - }, - { - field: 'nombreEmpleado', - headerName: 'Empleado', - flex: 1, - }, - { - field: 'fechaOrden', - headerName: 'Fecha', - flex: 1, - }, - { - field: 'estatusPedido', - headerName: 'Estatus', - flex: 1, - }, - { - field: 'precioTotal', - headerName: 'Precio Total', - flex: 1, - }, - { - field: 'estatusPago', - headerName: 'Pago', - flex: 1, - }, - { - field: 'estatusEnvio', - headerName: 'Envio', - flex: 1, - }, + { field: 'pedido', headerName: 'Pedido ID', flex: 1 }, + { field: 'nombreEmpleado', headerName: 'Empleado', flex: 1 }, + { field: 'fechaOrden', headerName: 'Fecha', flex: 1 }, + { field: 'estatusPedido', headerName: 'Estatus', flex: 1 }, + { field: 'precioTotal', headerName: 'Precio Total', flex: 1 }, + { field: 'estatusPago', headerName: 'Pago', flex: 1 }, + { field: 'estatusEnvio', headerName: 'Envio', flex: 1 }, + ]; const botones = [ @@ -148,14 +152,18 @@ const ListaPedidos = () => { rows={filas} disableRowSelectionOnClick={true} checkboxSelection + onRowClick={({ row }) => abrirModalEditar(row)} onRowSelectionModelChange={(seleccion) => { - const ids = Array.isArray(seleccion) ? seleccion : Array.from(seleccion?.ids || []); + const ids = Array.isArray(seleccion) + ? seleccion + : Array.from(seleccion?.ids || []); setSeleccionados(ids); }} /> )} + {alerta && ( { onClose={() => setAlerta(null)} /> )} + + + {/* 🆕 Modal Editar Pedido */} + ); }; -export default ListaPedidos; +export default ListaPedidos; \ No newline at end of file diff --git a/src/Vistas/Paginas/Productos/ListaProductos.jsx b/src/Vistas/Paginas/Productos/ListaProductos.jsx index 514e84de..dce165ea 100644 --- a/src/Vistas/Paginas/Productos/ListaProductos.jsx +++ b/src/Vistas/Paginas/Productos/ListaProductos.jsx @@ -1,8 +1,10 @@ //RF[26] Crea Producto - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF26] //RF[27] Consulta Lista de Productos - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF27] //RF[30] Elimina Producto - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF30] +//RF[58] - Exportar Productos - [https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF58] + import { Box, useTheme, Chip } from '@mui/material'; -import { useState, useCallback } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import Tabla from '@Organismos/Tabla'; import ContenedorLista from '@Organismos/ContenedorLista'; import Alerta from '@Moleculas/Alerta'; @@ -14,21 +16,93 @@ import { useEliminarProductos } from '@Hooks/Productos/useEliminarProductos'; import { tokens } from '@SRC/theme'; import { useAuth } from '@Hooks/AuthProvider'; import { PERMISOS } from '@Constantes/permisos'; +import ModalImportarProdctos from '@Organismos/ModalImportarProductos'; +import useImportarProductos from '@Hooks/Productos/useImportarProductos'; +import ModalFlotante from '@Organismos/ModalFlotante.jsx'; +import InfoProducto from '@Moleculas/InfoProducto.jsx'; +import { useLeerProducto } from '@Hooks/Productos/useLeerProducto.js'; +import useExportarProductos from '@Hooks/Productos/useExportarProductos'; + const ListaProductos = () => { const { productos, cargando, error, recargar } = useConsultarProductos(); const { eliminar } = useEliminarProductos(); const theme = useTheme(); const colores = tokens(theme.palette.mode); const { usuario } = useAuth(); - // prettier-ignore - const MENSAJE_POPUP_ELIMINAR - = '¿Estás seguro de que deseas eliminar los productos seleccionados? Esta acción no se puede deshacer.'; const [productosSeleccionados, setProductosSeleccionados] = useState([]); + const [productoDetalleSeleccionado, setProductoDetalleSeleccionado] = useState(null); const [mostrarModalProveedor, setMostrarModalProveedor] = useState(false); const [mostrarModalProducto, setMostrarModalProducto] = useState(false); const [alerta, setAlerta] = useState(null); 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 [openModalExportar, setAbrirPopUpExportar] = useState(false); + const MENSAJE_POPUP_EXPORTAR = '¿Deseas exportar la lista de productos? El archivo será generado en formato .xlsx'; + const manejarCancelarExportar = () => { + setAbrirPopUpExportar(false); + }; + + const manejarConfirmarExportar = async () => { + if (productosSeleccionados.length === 0) { + setAlerta({ + tipo: 'warning', + mensaje: 'Selecciona al menos un producto para exportar.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + return; + } + + await exportar(productosSeleccionados); + setAbrirPopUpExportar(false); + }; + + const { exportar, error: errorExportar, mensaje } = useExportarProductos(); + + useEffect(() => { + if (errorExportar) { + setAlerta({ + tipo: 'error', + mensaje: errorExportar, + icono: true, + cerrable: true, + centradoInferior: true, + }); + } + }, [errorExportar]); + + useEffect(() => { + if (mensaje) { + setAlerta({ + tipo: 'success', + mensaje, + icono: true, + cerrable: true, + centradoInferior: true, + }); + } + }, [mensaje]); + + const { + detalleProducto, + cargando: cargandoDetalle, + error: errorDetalle, + } = useLeerProducto(productoDetalleSeleccionado); + + // Efecto: cuando se cierre el modal, se limpia el producto seleccionado + useEffect(() => { + if (!abrirModalDetalle) { + setProductoDetalleSeleccionado(null); + } + }, [abrirModalDetalle]); const mostrarFormularioProducto = useCallback(() => { setMostrarModalProducto(true); @@ -83,6 +157,8 @@ const ListaProductos = () => { } }; + const handleAbrirImportar = () => setModalImportarAbierto(true); + const columnas = [ { field: 'imagen', @@ -90,7 +166,7 @@ const ListaProductos = () => { flex: 0.5, renderCell: (params) => ( Producto @@ -118,10 +194,10 @@ const ListaProductos = () => { renderCell: ({ row: { estado } }) => ( @@ -143,24 +219,26 @@ const ListaProductos = () => { onClick: mostrarFormularioProducto, size: 'large', backgroundColor: colores.altertex[1], + disabled: !usuario?.permisos?.includes(PERMISOS.CREAR_PRODUCTO), }, { - //variant: 'outlined', + variant: 'outlined', label: 'Importar', - onClick: () => console.log('Importar'), + onClick: handleAbrirImportar, color: 'primary', size: 'large', - outlineColor: colores.primario[10], - construccion: true, + outlineColor: colores.altertex[1], + disabled: !usuario?.permisos?.includes(PERMISOS.IMPORTAR_PRODUCTOS), + }, { - //variant: 'outlined', + variant: 'outlined', label: 'Exportar', - onClick: () => console.log('Exportar'), + onClick: () => setAbrirPopUpExportar(true), color: 'primary', + outlineColor: colores.altertex[1], size: 'large', - outlineColor: colores.primario[10], - construccion: true, + disabled: !usuario?.permisos?.includes(PERMISOS.EXPORTAR_PRODUCTOS), }, { label: 'Eliminar', @@ -187,8 +265,8 @@ const ListaProductos = () => { return ( <> {mostrarModalProducto && ( @@ -204,8 +282,10 @@ const ListaProductos = () => { alCerrarFormularioProveedor={cerrarFormularioProveedor} /> )} - - {error && } + + {error && ( + + )} { checkboxSelection rowHeight={80} onRowSelectionModelChange={(nuevosIds) => { - const ids = Array.isArray(nuevosIds) ? nuevosIds : Array.from(nuevosIds?.ids || []); + 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) + setAbrirModalDetalle(true); + }} /> @@ -237,8 +323,62 @@ const ListaProductos = () => { abrir={openModalEliminar} cerrar={manejarCancelarEliminar} confirmar={manejarConfirmarEliminar} - dialogo={MENSAJE_POPUP_ELIMINAR} + dialogo="¿Estás seguro de que deseas eliminar los productos seleccionados? Esta acción no se puede deshacer." /> + setModalImportarAbierto(false)} + onConfirm={importar} + cargando={cargandoImportacion} + errores={errores} + exito={exito} + recargar={recargar} + > + + + + {abrirModalDetalle && ( + setAbrirModalDetalle(false)} + titulo={detalleProducto?.nombreComun || 'Cargando...'} + tituloVariant='h4' + customWidth={950} + botones={[ + { + label: 'Editar', + variant: 'contained', + color: 'primary', + backgroundColor: colores.altertex[1], + onClick: () => console.log('Editar producto'), + construccion: true, + }, + { + label: 'Salir', + variant: 'outlined', + color: 'primary', + outlineColor: colores.primario[1], + onClick: () => setAbrirModalDetalle(false), + }, + ]} + > + {cargandoDetalle ? ( +

Cargando información del producto...

+ ) : errorDetalle ? ( +

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

+ ) : ( + + )} +
+ )} ); }; diff --git a/src/Vistas/Paginas/Roles/ListaRoles.jsx b/src/Vistas/Paginas/Roles/ListaRoles.jsx index 784fe807..103a0ed5 100644 --- a/src/Vistas/Paginas/Roles/ListaRoles.jsx +++ b/src/Vistas/Paginas/Roles/ListaRoles.jsx @@ -14,6 +14,7 @@ import Alerta from '@Moleculas/Alerta'; import PopUp from '@Moleculas/PopUp'; import { useEliminarRol } from '@Hooks/Roles/useEliminarRol'; import NavegadorAdministrador from '@Organismos/NavegadorAdministrador'; +import ModalDetalleRol from '@Organismos/ModalDetalleRol'; const estiloImagenLogo = { marginRight: '1rem' }; // ID del superusuario que no debe ser eliminado @@ -29,6 +30,8 @@ const ListaRoles = () => { const MENSAJE_POPUP_ELIMINAR = '¿Estás seguro de que deseas eliminar los roles seleccionados?'; + const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); + const [rolSeleccionado, setRolSeleccionado] = useState(null); const [modalCrearAbierto, setModalCrearAbierto] = useState(false); const [alerta, setAlerta] = useState(null); const { eliminar } = useEliminarRol(); @@ -47,7 +50,7 @@ const ListaRoles = () => { icono: true, cerrable: true, centradoInferior: true, - duracion: 3000, + duracion: 2500, }); recargar(); }; @@ -57,7 +60,7 @@ const ListaRoles = () => { }; const verificarSeleccion = (seleccion) => { - const IDS_PROTEGIDOS = [SUPERUSER_ID, SUPERVISOR_ID, EMPLEADO_ID]; + const IDS_PROTEGIDOS = [SUPERUSER_ID, SUPERVISOR_ID, EMPLEADO_ID]; const seleccionSinSuperuser = seleccion.filter((id) => !IDS_PROTEGIDOS.includes(Number(id))); if (seleccion.length !== seleccionSinSuperuser.length) { @@ -68,7 +71,7 @@ const ListaRoles = () => { icono: true, cerrable: true, centradoInferior: true, - duracion: 3000, + duracion: 2500, }); } @@ -95,7 +98,7 @@ const ListaRoles = () => { icono: true, cerrable: true, centradoInferior: true, - duracion: 3000, + duracion: 2500, }); } else { setAlerta({ @@ -104,7 +107,6 @@ const ListaRoles = () => { icono: true, cerrable: true, centradoInferior: true, - duracion: 3000, }); } @@ -118,11 +120,11 @@ const ListaRoles = () => { console.error('Error al eliminar roles:', error); setAlerta({ tipo: 'error', - mensaje: error.message || 'Ocurrió un error al eliminar los roles. Puedes intentarlo de nuevo.', + mensaje: error.message || 'Ocurrió un error al eliminar los roles. Puedes intentarlo de nuevo.', icono: true, cerrable: true, centradoInferior: true, - duracion: 3000, + duracion: 2500, }); } finally { setAbrirPopupEliminar(false); @@ -193,8 +195,8 @@ const ListaRoles = () => { if (seleccionFiltrada.length > 0) { setAbrirPopupEliminar(true); } else if ( - seleccionFiltrada.length === 0 - && seleccionados.some(id => IDS_PROTEGIDOS.includes(Number(id))) + seleccionFiltrada.length === 0 + && seleccionados.some(id => IDS_PROTEGIDOS.includes(Number(id))) ) { setAlerta({ tipo: 'warning', @@ -203,7 +205,7 @@ const ListaRoles = () => { icono: true, cerrable: true, centradoInferior: true, - duracion: 3000, + duracion: 2500, }); } } @@ -279,6 +281,10 @@ const ListaRoles = () => { loading={cargando} disableRowSelectionOnClick={true} checkboxSelection + onRowClick={(params) => { + setRolSeleccionado(params.id); + setModalDetalleAbierto(true); + }} onRowSelectionModelChange={(seleccion) => { const ids = Array.isArray(seleccion) ? seleccion : Array.from(seleccion?.ids || []); setSeleccionados(ids); @@ -298,6 +304,15 @@ const ListaRoles = () => { + { + setModalDetalleAbierto(false); + recargar() + }} + idRol={rolSeleccionado} + /> + { const { setsDeProductos, cargando, error, recargar } = useConsultarSetsProductos(); @@ -29,6 +33,9 @@ const ListaSetsProductos = () => { const [abrirEliminar, setAbrirPopUpEliminar] = useState(false); const [setSeleccionado, setSetSeleccionado] = useState(null); const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); + const [modalCrearAbierto, setModalCrearAbierto] = useState(false); + const [abrirModalEditar, setAbrirModalEditar] = useState(false); + const [formData, setFormData] = useState(null); const { eliminar } = useEliminarSetProductos(); const { usuario } = useAuth(); @@ -61,6 +68,65 @@ const ListaSetsProductos = () => { } }; + const handleCerrarModalCrear = () => { + setModalCrearAbierto(false); + }; + + const handleSetProductoCreadoExitosamente = () => { + handleCerrarModalCrear(); + recargar(); + }; + + const handleFormDataChange = (data) => { + setFormData(data); + }; + + const { actualizarSet } = useActualizarSetsProductos(); + + const esValido = (data) => { + return data?.nombre?.trim() !== '' && data?.descripcion?.trim() !== ''; + }; + + const handleGuardar = async () => { + if (!esValido(formData)) { + setAlerta({ + tipo: 'error', + mensaje: 'El nombre y la descripción son obligatorios.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + return; + } + + try { + await actualizarSet( + setSeleccionado.idSetProducto, + formData.nombre, + formData.descripcion, + formData.activo, + formData.productos + ); + await recargar(); + setAbrirModalEditar(false); + setAlerta({ + tipo: 'success', + mensaje: 'Set de productos actualizado correctamente.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + } catch (error) { + setAlerta({ + tipo: 'error', + mensaje: error?.message || 'Error al actualizar el set de productos.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + } + }; + const columns = [ { field: 'nombre', @@ -119,8 +185,8 @@ const ListaSetsProductos = () => { color: 'error', size: 'large', backgroundColor: colores.altertex[1], - onClick: () => console.log('Añadir'), - construccion: true, + onClick: () => setModalCrearAbierto(true), + disabled: !usuario?.permisos?.includes(PERMISOS.CREAR_SET_PRODUCTOS), }, { label: 'Eliminar', @@ -137,7 +203,7 @@ const ListaSetsProductos = () => { setAbrirPopUpEliminar(true); } }, - disabled: !usuario?.permisos?.includes(PERMISOS.ELIMINAR_GRUPO_EMPLEADOS), + disabled: !usuario?.permisos?.includes(PERMISOS.ELIMINAR_SET_PRODUCTOS), size: 'large', color: 'error', backgroundColor: colores.altertex[1], @@ -177,6 +243,12 @@ const ListaSetsProductos = () => { + + {alerta && ( { variant: 'contained', color: 'error', backgroundColor: colores.altertex[1], - construccion: true, + onClick: () => { + setModalDetalleAbierto(false); + setAbrirModalEditar(true); + }, }, { label: 'SALIR', @@ -228,7 +303,41 @@ const ListaSetsProductos = () => { nombre={''} descripcion={setSeleccionado.descripcion} productos={setSeleccionado.productos || []} - grupos={setSeleccionado.grupos || []} + idsProductos={setSeleccionado?.idsProductos || []} + /> + + )} + {abrirModalEditar && ( + setAbrirModalEditar(false)} + titulo='Editar Set de Productos' + tituloVariant='h4' + customWidth={890} + botones={[ + { + label: 'Guardar', + variant: 'contained', + color: 'primary', + backgroundColor: colores.altertex[1], + onClick: handleGuardar, + }, + { + label: 'Cancelar', + variant: 'outlined', + color: 'primary', + outlineColor: colores.altertex[1], + onClick: () => setAbrirModalEditar(false), + }, + ]} + > + )} diff --git a/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx b/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx index 521e0699..2b2d4e79 100644 --- a/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx +++ b/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx @@ -9,6 +9,7 @@ import Chip from '@Atomos/Chip'; import { useConsultarListaUsuarios } from '@Hooks/Usuarios/useConsultarListaUsuarios'; import { useEliminarUsuarios } from '@Hooks/Usuarios/useEliminarUsuarios'; import { useConsultarRoles } from '@Hooks/Roles/useConsultarRoles'; +import { useConsultarClientes } from '@Hooks/Clientes/useConsultarClientes'; import { RUTAS } from '@Constantes/rutas'; import { tokens } from '@SRC/theme'; import NavegadorAdministrador from '@Organismos/NavegadorAdministrador'; @@ -21,6 +22,7 @@ import { useTheme } from '@mui/material'; import { useActivar2FA } from '@Hooks/Usuarios/useActivar2FA'; import Activar2FAModal from '@Organismos/Activar2FAModal'; import Verificar2FAModal from '@Moleculas/Verificar2FAModal'; +import ModalActualizarUsuario from '@Organismos/ModalActualizarUsuario'; const estiloImagenLogo = { marginRight: '1rem' }; @@ -36,6 +38,7 @@ const estiloImagenLogo = { marginRight: '1rem' }; * @see [RF02 Super Administrador Consulta Lista de Usuarios](https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF2) * @see [RF03 Leer usuario](https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF3) * @see [RF05 Super Administrador Eliminar Usuario](https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF5) + * @see [RF04 Super Administrador Actualiza Usuario](https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF4) */ const ListaUsuarios = () => { @@ -44,21 +47,25 @@ const ListaUsuarios = () => { const navigate = useNavigate(); const [alerta, setAlerta] = useState(null); const { usuarios, cargando, error, recargar } = useConsultarListaUsuarios(); - const { roles} = useConsultarRoles(); + const { clientes } = useConsultarClientes(); + const { roles } = useConsultarRoles(); const { usuario: usuarioAutenticado } = useAuth(); const [modalCrearUsuarioAbierto, setModalCrearUsuarioAbierto] = useState(false); const [idUsuarioSeleccionado, setIdUsuarioSeleccionado] = useState(null); const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); + const [modalActualizarAbierto, setModalActualizarAbierto] = useState(false); const { usuario, cargando: cargandoDetalle, error: errorDetalle, } = useUsuarioId(modalDetalleAbierto ? idUsuarioSeleccionado : null); - const [modal2FAAbierto, setModal2FAAbierto] = useState(false); - const { qrCode, cargando: cargandoQR, error: errorQR, setQrCode } = useActivar2FA(); + const [modal2FAAbierto, setModal2FAAbierto] = useState(false); + const { qrCode, cargando: cargandoQR, error: errorQR, setQrCode } = useActivar2FA(); + const rolesPorId = Object.fromEntries(roles.map((fila) => [fila.idRol, fila.nombre])); - /** const manejarActivar2FA = async () => { + + /** const manejarActivar2FA = async () => { await activar2FA({ idUsuario: usuarioAutenticado?.idUsuario, nombre: usuarioAutenticado?.nombre, @@ -68,15 +75,10 @@ const ListaUsuarios = () => { }; */ const opcionesRol = roles.map((rol) => ({ - value: rol.idRol, + value: rol.idRol, label: rol.nombre, })); - const obtenerIdRolPorNombre = (nombreRol) => { - const rolEncontrado = roles.find((rol) => rol.nombre === nombreRol); - return rolEncontrado ? rolEncontrado.idRol : ''; - }; - const redirigirAInicio = () => { navigate(RUTAS.SISTEMA_ADMINISTRATIVO.BASE, { replace: true }); }; @@ -85,21 +87,21 @@ const ListaUsuarios = () => { await cerrarSesion(); }; const { cerrarSesion } = useAuth(); -const { - usuariosAEliminar, - abrirPopUp, - abrirModal2FA, - error2FA, - cargando2FA, - manejarSeleccion, - manejarAbrirPopUp, - manejarCerrarPopUp, - eliminarUsuarios, - manejarVerificar2FA, - manejarCerrarModal2FA, - codigo2FA, - setCodigo2FA -} = useEliminarUsuarios(setAlerta, recargar); + const { + usuariosAEliminar, + abrirPopUp, + abrirModal2FA, + error2FA, + cargando2FA, + manejarSeleccion, + manejarAbrirPopUp, + manejarCerrarPopUp, + eliminarUsuarios, + manejarVerificar2FA, + manejarCerrarModal2FA, + codigo2FA, + setCodigo2FA, + } = useEliminarUsuarios(setAlerta, recargar); useEffect(() => {}, [usuariosAEliminar]); @@ -196,7 +198,7 @@ const { id, idUsuario: id, nombre: usuario.nombre, - rol: usuario.rol || 'Sin rol', + rol: rolesPorId[usuario.rol] || 'Sin rol', cliente: usuario.cliente ? [usuario.cliente] : [], estatus: usuario.estatus, correo: usuario.correo, @@ -257,7 +259,8 @@ const { backgroundColor: colores.verde[1], disabled: !usuarioAutenticado?.permisos?.includes(PERMISOS.ACTIVAR_2FA_SUPERADMIN), }, - */]; + */ + ]; const redirigirATienda = () => { navigate(RUTAS.SISTEMA_TIENDA.BASE, { replace: true }); @@ -310,16 +313,6 @@ const { } informacionBotones={botones} > - {alerta && ( - setAlerta(null)} - centradoInferior - /> - )} {modalCrearUsuarioAbierto && ( console.log('Editar usuario'), - disabled: true, + onClick: () => { + setModalDetalleAbierto(false); + setTimeout(() => setModalActualizarAbierto(true), 100); + }, + disabled: !usuarioAutenticado?.permisos?.includes(PERMISOS.ACTUALIZAR_USUARIO), }, { label: 'SALIR', @@ -404,34 +400,34 @@ const {

Cargando usuario...

) : usuario ? ( <> - cliente?.nombreCliente) - ? usuario.clientes - .filter((cliente) => cliente?.nombreCliente) - .map((cliente) => cliente.nombreCliente) - .join(', ') - : 'Sin cliente asignado' - } - rol={obtenerIdRolPorNombre(usuario.rol)} - datosContacto={{ - email: usuario.correoElectronico, - telefono: usuario.numeroTelefono, - direccion: usuario.direccion, - }} - datosAdicionales={{ - nacimiento: new Date(usuario.fechaNacimiento).toLocaleDateString('es-MX'), - genero: usuario.genero, - }} - estadoUsuario={{ - label: usuario.estatus === 1 ? 'Activo' : 'Inactivo', - color: 'primary', - shape: 'circular', - backgroundColor: 'rgba(24, 50, 165, 1)', - }} - opcionesRol={opcionesRol} - /> + cliente?.nombreCliente) + ? usuario.clientes + .filter((cliente) => cliente?.nombreCliente) + .map((cliente) => cliente.nombreCliente) + .join(', ') + : 'Sin cliente asignado' + } + rol={(usuario.rol)} + datosContacto={{ + email: usuario.correoElectronico, + telefono: usuario.numeroTelefono, + direccion: usuario.direccion, + }} + datosAdicionales={{ + nacimiento: new Date(usuario.fechaNacimiento).toLocaleDateString('es-MX'), + genero: usuario.genero, + }} + estadoUsuario={{ + label: usuario.estatus === 1 ? 'Activo' : 'Inactivo', + color: 'primary', + shape: 'circular', + backgroundColor: 'rgba(24, 50, 165, 1)', + }} + opcionesRol={opcionesRol} + /> ) : (

No se encontró información del usuario.

@@ -439,14 +435,43 @@ const { )} + {modalActualizarAbierto && usuario && ( + setModalActualizarAbierto(false)} + onAccion={() => { + recargar(); + }} + usuarioEdicion={{ + ...usuario, + cliente: usuario.clientes ? usuario.clientes.map((cliente) => cliente.idCliente) : [], + }} + roles={roles} + clientes={clientes} + esSuperAdmin={false} + cargandoRoles={false} + /> + )} + {errorDetalle && (
)} + {alerta && ( + setAlerta(null)} + /> + )} ); }; -export default ListaUsuarios; \ No newline at end of file +export default ListaUsuarios; diff --git a/src/hooks/AuthProvider.jsx b/src/hooks/AuthProvider.jsx index f95880e2..e55fcada 100644 --- a/src/hooks/AuthProvider.jsx +++ b/src/hooks/AuthProvider.jsx @@ -11,41 +11,78 @@ const AuthContext = createContext(null); export const AuthProvider = ({ children }) => { const [usuario, setUsuario] = useState(null); const [nombreUsuario, setNombreUsuario] = useState(() => { - // Obtener el nombre de usuario del localStorage const nombreUsuarioGuardado = localStorage.getItem('nombreUsuario'); return nombreUsuarioGuardado ? JSON.parse(nombreUsuarioGuardado) : null; }); const [cargando, setCargando] = useState(true); + const [csrfToken, setCsrfToken] = useState(null); const { toggleColorMode } = useMode(); + useEffect(() => { + const reqInterceptor = axios.interceptors.request.use( + (configuracion) => { + if (csrfToken && configuracion.method !== 'get') { + configuracion.headers['X-CSRF-Token'] = csrfToken; + } + return configuracion; + }, + (error) => Promise.reject(error) + ); + + const resInterceptor = axios.interceptors.response.use( + (response) => response, + async (error) => { + if (error.response?.status === 403 && error.response?.data?.code === 'EBADCSRFTOKEN') { + await obtenerCSRFToken(); + const reqOriginal = error.config; + reqOriginal.headers['X-CSRF-Token'] = csrfToken; + return axios.request(reqOriginal); + } + return Promise.reject(error); + } + ); + + return () => { + axios.interceptors.request.eject(reqInterceptor); + axios.interceptors.response.eject(resInterceptor); + }; + }, [csrfToken]); + useEffect(() => { verificarSesion(); + obtenerCSRFToken(); }, []); useEffect(() => { - // Guardar el nombre de usuario en el localStorage if (nombreUsuario) { localStorage.setItem('nombreUsuario', JSON.stringify(nombreUsuario)); } }, [nombreUsuario]); + const obtenerCSRFToken = async () => { + try { + const respuesta = await axios.get(`${API_URL}/api/csrf-token`, { + headers: { 'x-api-key': API_KEY }, + withCredentials: true, + }); + console.log(respuesta) + setCsrfToken(respuesta.data.csrfToken); + } catch (error) { + console.error('Error al obtener token CSRF:', error); + } + }; + const resetearTema = () => { - // Obtener el tema actual del localStorage const temaActual = localStorage.getItem('tema'); - - // Si el tema actual es oscuro, cambiarlo a claro if (temaActual && JSON.parse(temaActual) === 'dark') { toggleColorMode(); } - - // Borrar el tema del localStorage localStorage.removeItem('tema'); }; const cerrarSesion = async () => { const resultado = await Swal.fire({ title: '¿Estás seguro que quieres cerrar sesión?', - showCancelButton: true, confirmButtonColor: 'rgba(24, 50, 165, 1)', cancelButtonColor: '#c62828', @@ -69,6 +106,7 @@ export const AuthProvider = ({ children }) => { console.error('Error al cerrar sesión:', error); } finally { setUsuario(null); + setCsrfToken(null); } }; @@ -87,18 +125,20 @@ export const AuthProvider = ({ children }) => { }; return ( - {children} ); }; -export const useAuth = () => useContext(AuthContext); +export const useAuth = () => useContext(AuthContext); \ No newline at end of file diff --git a/src/hooks/Categorias/useActualizarCategoria.js b/src/hooks/Categorias/useActualizarCategoria.js new file mode 100644 index 00000000..1818ae2d --- /dev/null +++ b/src/hooks/Categorias/useActualizarCategoria.js @@ -0,0 +1,62 @@ +import { useState } from 'react'; +import { RepositorioActualizarCategoria } from '@Repositorios/Categorias/repositorioActualizarCategoria'; + +/** + * Hook personalizado para manejar la actualización de una categoría. + * + * RF49 - Actualizar Categoría + * @returns {object} Funciones y estados relacionados con la actualización. + */ +const useActualizarCategoria = () => { + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(null); + const [exitoso, setExitoso] = useState(false); + const [mensaje, setMensaje] = useState(''); + + /** + * Ejecuta la actualización de una categoría llamando al repositorio. + * + * @param {number} idCategoria - ID de la categoría a actualizar. + * @param {object} datosCategoria - Datos nuevos para la categoría. + * @returns {Promise} - Resultado de la operación. + */ + const actualizarCategoria = async (idCategoria, datosCategoria) => { + setCargando(true); + setError(null); + setExitoso(false); + setMensaje(''); + + try { + const respuesta = await RepositorioActualizarCategoria.actualizar(idCategoria, datosCategoria); + setMensaje(respuesta.mensaje || 'Categoría actualizada exitosamente'); + setExitoso(true); + return { success: true, data: respuesta }; + } catch (err) { + const mensajeError = err.message || 'Error al actualizar la categoría'; + setError(mensajeError); + return { success: false, error: mensajeError }; + } finally { + setCargando(false); + } + }; + + /** + * Limpia los estados de error, éxito y mensaje del hook. + */ + const limpiarEstado = () => { + setError(null); + setExitoso(false); + setMensaje(''); + }; + + return { + actualizarCategoria, + cargando, + error, + exitoso, + mensaje, + limpiarEstado, + }; +}; + +export default useActualizarCategoria; diff --git a/src/hooks/Categorias/useLeerCategoria.js b/src/hooks/Categorias/useLeerCategoria.js new file mode 100644 index 00000000..c55a9e76 --- /dev/null +++ b/src/hooks/Categorias/useLeerCategoria.js @@ -0,0 +1,34 @@ +// RF48 - Leer Categoría de Productos +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +/** + * Consulta el detalle de una categoría por su ID + * @param {number} idCategoria + * @returns {Promise} + */ +export const leerCategoria = async (idCategoria) => { + try { + const { data } = await axios.get( + `${RUTAS_API.CATEGORIAS.LEER}/${idCategoria}`, + { + headers: { 'x-api-key': API_KEY }, + withCredentials: true, + } + ); + + const productos = data.categoria.productos?.map((produc) => produc.nombreComun) || []; + + return { + idCategoria: data.categoria.idCategoria, + nombreCategoria: data.categoria.nombreCategoria, + descripcion: data.categoria.descripcion, + productos, + }; + } catch (error) { + const mensaje = error.response?.data?.mensaje || 'Error al obtener categoría'; + throw new Error(mensaje); + } +}; \ No newline at end of file diff --git a/src/hooks/Cuotas/useActualizarCuota.js b/src/hooks/Cuotas/useActualizarCuota.js new file mode 100644 index 00000000..146602a6 --- /dev/null +++ b/src/hooks/Cuotas/useActualizarCuota.js @@ -0,0 +1,35 @@ +import { useState } from 'react'; +import axios from 'axios'; + +const API_KEY = import.meta.env.VITE_API_KEY; +const BASE_URL = import.meta.env.VITE_API_URL; + +export const useActualizarCuota = () => { + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(false); + + const actualizarCuota = async (datos) => { + setCargando(true); + setError(false); + + try { + const response = await axios.put( + `${BASE_URL}/api/cuotas/actualizar-set-cuotas`, + datos, + { + headers: { 'x-api-key': API_KEY }, + withCredentials: true, + } + ); + + return response.data; + } catch (err) { + setError(true); + throw new Error(err.response?.data?.mensaje || 'Error al actualizar'); + } finally { + setCargando(false); + } + }; + + return { actualizarCuota, cargando, error }; +}; \ No newline at end of file diff --git a/src/hooks/Cuotas/useLeerCuota.js b/src/hooks/Cuotas/useLeerCuota.js new file mode 100644 index 00000000..9b1f5eb9 --- /dev/null +++ b/src/hooks/Cuotas/useLeerCuota.js @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react'; +import { RepositorioLeerCuota } from '@Repositorios/Cuotas/repositorioLeerCuota'; + +/** + * Hook para obtener los datos de una cuota por su ID + * @param {number} idCuota - ID de la cuota a consultar + * @returns {{ + * cuota: CuotaLectura | null, + * mensaje: string, + * cargando: boolean, + * error: string | null + * }} + * + * @see [RF[33] Leer cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF33] + * */ + +export const useCuotaId = (idCuota) => { + const [cuota, setCuota] = useState(null); + const [mensaje, setMensaje] = useState(''); + const [cargando, setCargando] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const obtenerCuota = async () => { + setCargando(true); + setError(null); + + try { + const { cuota, mensaje } = await RepositorioLeerCuota.obtenerPorId(idCuota); + + setCuota(cuota); + setMensaje(mensaje); + } catch (err) { + setError(err.message); + } finally { + setCargando(false); + } + }; + + if (idCuota) { + obtenerCuota(); + } + }, [idCuota]); + + return { cuota, mensaje, cargando, error }; +}; diff --git a/src/hooks/Cuotas/useObtenerOpcionesCuotas.js b/src/hooks/Cuotas/useObtenerOpcionesCuotas.js new file mode 100644 index 00000000..862580e8 --- /dev/null +++ b/src/hooks/Cuotas/useObtenerOpcionesCuotas.js @@ -0,0 +1,39 @@ +import { useState, useEffect } from 'react'; +import { useAuth } from '@Hooks/AuthProvider'; +import obtenerProductos from '@Servicios/obtenerProductos'; + +export const useObtenerOpcionesCuotas = () => { + const { usuario } = useAuth(); + const idCliente = usuario?.clienteSeleccionado; + + const [opciones, setOpciones] = useState([]); + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(null); + + + + useEffect(() => { + if (!idCliente) { + return; + } + + const cargarOpciones = async () => { + setCargando(true); + setError(null); + + try { + const productos = await obtenerProductos(idCliente); + setOpciones(productos); + } catch { + setError('No se pudieron cargar las opciones de productos'); + setOpciones([]); + } finally { + setCargando(false); + } + }; + + cargarOpciones(); + }, [idCliente]); + + return { opciones, cargando, error }; +}; diff --git a/src/hooks/Empleados/useAccionesEmpleado.js b/src/hooks/Empleados/useAccionesEmpleado.js deleted file mode 100644 index 6825a653..00000000 --- a/src/hooks/Empleados/useAccionesEmpleado.js +++ /dev/null @@ -1,212 +0,0 @@ -//RF19 - Actualizar empleado - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF19 -import { useState } from 'react'; -import dayjs from 'dayjs'; -import { RepositorioActualizarEmpleado } from '@Repositorios/Empleados/RepositorioActualizarEmpleado'; -import { validarDatosActualizarEmpleado } from '@Modelos/Empleados/ActualizarEmpleado'; - -export const useAccionesEmpleado = (empleadoInicial = null) => { - const esEdicion = !!empleadoInicial; - const [erroresValidacion, setErroresValidacion] = useState({}); - const [cargando, setCargando] = useState(false); - const [alerta, setAlerta] = useState(null); - - // Inicializar con datos de edición o vacío - const [datosEmpleado, setDatosEmpleado] = useState(() => { - if (esEdicion) { - let fechaAntiguedad = null; - if (empleadoInicial.antiguedadDate) { - fechaAntiguedad = dayjs(empleadoInicial.antiguedadDate); - } - return { - ...empleadoInicial, - idEmpleado: empleadoInicial.id, - nombreCompleto: empleadoInicial.nombreCompleto, - correoElectronico: empleadoInicial.correoElectronico, - antiguedad: fechaAntiguedad, - }; - } - - return { - nombre: '', - nombreCompleto: '', - correoElectronico: '', - idEmpleado: '', - idUsuario: '', - numeroEmergencia: '', - areaTrabajo: '', - posicion: '', - cantidadPuntos: '', - antiguedad: null, - }; - }); - - const CAMPO_OBLIGATORIO = 'Este campo es obligatorio'; - - // Función para verificar si se han realizado cambios - const hayCambios = () => { - if (!esEdicion) return true; // Si es creación, siempre hay cambios - - const camposAComparar = [ - 'nombreCompleto', - 'correoElectronico', - 'numeroEmergencia', - 'areaTrabajo', - 'posicion', - 'cantidadPuntos', - ]; - - // Comparar campos simples - const cambioCamposSimples = camposAComparar.some( - (campo) => datosEmpleado[campo]?.toString() !== empleadoInicial[campo]?.toString() - ); - - // Comparar fecha de antigüedad - const fechaAntigua = empleadoInicial.antiguedadDate - ? dayjs(empleadoInicial.antiguedadDate).format('YYYY-MM-DD') - : null; - const fechaNueva = datosEmpleado.antiguedad - ? datosEmpleado.antiguedad.format('YYYY-MM-DD') - : null; - const cambioFecha = fechaAntigua !== fechaNueva; - - return cambioCamposSimples || cambioFecha; - }; - - // Métodos para manejar cambios - const manejarCambio = (evento) => { - const { name, value } = evento.target; - - if (name === 'numeroEmergencia') { - const soloNumeros = value.replace(/\D/g, '').slice(0, 10); - setDatosEmpleado((prev) => ({ ...prev, [name]: soloNumeros })); - } else { - setDatosEmpleado((prev) => ({ ...prev, [name]: value })); - } - }; - - const manejarAntiguedad = (nuevaFecha) => { - setDatosEmpleado((prev) => ({ - ...prev, - antiguedad: nuevaFecha, - })); - }; - - const obtenerHelperText = (campo) => { - const err = erroresValidacion[campo]; - if (err) { - return typeof err === 'string' ? err : CAMPO_OBLIGATORIO; - } - if ( - !esEdicion - && [ - 'idEmpleado', - 'idUsuario', - 'numeroEmergencia', - 'areaTrabajo', - 'posicion', - 'cantidadPuntos', - 'antiguedad', - ].includes(campo) - ) { - return CAMPO_OBLIGATORIO; - } - return ''; - }; - - const handleGuardar = async () => { - if (esEdicion && !hayCambios()) { - setAlerta({ - tipo: 'warning', - mensaje: 'No se han realizado cambios', - }); - return { exito: false, mensaje: 'No se han realizado cambios' }; - } - - const datosProcesados = { - ...datosEmpleado, - id: esEdicion ? empleadoInicial.id : datosEmpleado.idEmpleado, - nombreCompleto: datosEmpleado.nombreCompleto || datosEmpleado.nombre, - correoElectronico: datosEmpleado.correoElectronico, - antiguedad: datosEmpleado.antiguedad?.format('YYYY-MM-DD'), - }; - - const erroresCampos = {}; - if (!datosProcesados.areaTrabajo || datosProcesados.areaTrabajo.trim() === '') { - erroresCampos.areaTrabajo = 'El campo no puede estar vacío ni contener sólo espacios'; - } - if (!datosProcesados.posicion || datosProcesados.posicion.trim() === '') { - erroresCampos.posicion = 'El campo no puede estar vacío ni contener sólo espacios'; - } - - const nuevosErrores = { - ...erroresCampos, - ...validarDatosActualizarEmpleado(datosProcesados), - }; - - if (Object.keys(nuevosErrores).length > 0) { - setErroresValidacion(nuevosErrores); - setAlerta({ - tipo: 'error', - mensaje: 'Corrige los errores en el formulario antes de guardar.', - }); - return { exito: false }; - } - - setErroresValidacion({}); - setAlerta(null); - setCargando(true); - - try { - await RepositorioActualizarEmpleado.actualizar(datosProcesados); - setAlerta({ - tipo: 'success', - mensaje: esEdicion ? 'Empleado actualizado correctamente' : 'Empleado creado correctamente', - }); - return { - exito: true, - mensaje: esEdicion ? 'Empleado actualizado correctamente' : 'Empleado creado correctamente', - }; - } catch (error) { - setAlerta({ - tipo: 'error', - mensaje: error.message || 'Error al guardar el empleado', - }); - return { exito: false, mensaje: error.message || 'Error al guardar el empleado' }; - } finally { - setCargando(false); - } - }; - - const limpiarFormulario = () => { - setDatosEmpleado({ - nombre: '', - nombreCompleto: '', - correoElectronico: '', - idEmpleado: '', - idUsuario: '', - numeroEmergencia: '', - areaTrabajo: '', - posicion: '', - cantidadPuntos: '', - antiguedad: null, - }); - setErroresValidacion({}); - setAlerta(null); - }; - - return { - datosEmpleado, - setDatosEmpleado, - erroresValidacion, - alerta, - setAlerta, - cargando, - esEdicion, - manejarCambio, - manejarAntiguedad, - obtenerHelperText, - handleGuardar, - limpiarFormulario, - CAMPO_OBLIGATORIO, - }; -}; diff --git a/src/hooks/Empleados/useActualizarEmpleado.js b/src/hooks/Empleados/useActualizarEmpleado.js index cbf6abe2..4dbfd431 100644 --- a/src/hooks/Empleados/useActualizarEmpleado.js +++ b/src/hooks/Empleados/useActualizarEmpleado.js @@ -1,26 +1,188 @@ -//RF[19] Super Administrador Actualiza Empleado - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF19] - +//RF19 - Actualizar empleado - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF19 import { useState } from 'react'; +import dayjs from 'dayjs'; import { RepositorioActualizarEmpleado } from '@Repositorios/Empleados/RepositorioActualizarEmpleado'; +import { validarDatosActualizarEmpleado } from '@Modelos/Empleados/ActualizarEmpleado'; -export function useActualizarEmpleado() { +export const useActualizarEmpleado = (empleadoInicial = null) => { + const esEdicion = !!empleadoInicial; + const [erroresValidacion, setErroresValidacion] = useState({}); const [cargando, setCargando] = useState(false); - const [error, setError] = useState(null); - const [mensaje, setMensaje] = useState(''); + const [alerta, setAlerta] = useState(null); + + // Inicializar con datos de edición o vacío + const [datosEmpleado, setDatosEmpleado] = useState(() => { + if (esEdicion) { + let fechaAntiguedad = null; + if (empleadoInicial.antiguedadDate) { + fechaAntiguedad = dayjs(empleadoInicial.antiguedadDate); + } + return { + ...empleadoInicial, + idEmpleado: empleadoInicial.id, + antiguedad: fechaAntiguedad, + }; + } + + return { + numeroEmergencia: '', + areaTrabajo: '', + posicion: '', + cantidadPuntos: '', + antiguedad: null, + }; + }); + + const CAMPO_OBLIGATORIO = 'Este campo es obligatorio'; + + // Función para verificar si se han realizado cambios + const hayCambios = () => { + if (!esEdicion) return true; // Si es creación, siempre hay cambios + + const camposAComparar = ['numeroEmergencia', 'areaTrabajo', 'posicion', 'cantidadPuntos']; + + // Comparar campos simples + const cambioCamposSimples = camposAComparar.some( + (campo) => datosEmpleado[campo]?.toString() !== empleadoInicial[campo]?.toString() + ); + + // Comparar fecha de antigüedad + const fechaAntigua = empleadoInicial.antiguedadDate + ? dayjs(empleadoInicial.antiguedadDate).format('YYYY-MM-DD') + : null; + const fechaNueva = datosEmpleado.antiguedad + ? datosEmpleado.antiguedad.format('YYYY-MM-DD') + : null; + const cambioFecha = fechaAntigua !== fechaNueva; + + return cambioCamposSimples || cambioFecha; + }; + + // Métodos para manejar cambios + const manejarCambio = (evento) => { + const { name, value } = evento.target; + + if (name === 'numeroEmergencia') { + const soloNumeros = value.replace(/\D/g, '').slice(0, 10); + setDatosEmpleado((prev) => ({ ...prev, [name]: soloNumeros })); + } else { + setDatosEmpleado((prev) => ({ ...prev, [name]: value })); + } + }; + + const manejarAntiguedad = (nuevaFecha) => { + setDatosEmpleado((prev) => ({ + ...prev, + antiguedad: nuevaFecha, + })); + }; - const actualizar = async (cambios) => { + const obtenerHelperText = (campo) => { + const err = erroresValidacion[campo]; + if (err) { + return typeof err === 'string' ? err : CAMPO_OBLIGATORIO; + } + if ( + !esEdicion + && ['numeroEmergencia', 'areaTrabajo', 'posicion', 'cantidadPuntos', 'antiguedad'].includes( + campo + ) + ) { + return CAMPO_OBLIGATORIO; + } + return ''; + }; + + const handleGuardar = async () => { + if (esEdicion && !hayCambios()) { + setAlerta({ + tipo: 'warning', + mensaje: 'No se han realizado cambios', + }); + return { exito: false, mensaje: 'No se han realizado cambios' }; + } + + const datosProcesados = { + ...datosEmpleado, + id: esEdicion ? empleadoInicial.id : datosEmpleado.idEmpleado, + nombreCompleto: datosEmpleado.nombreCompleto || datosEmpleado.nombre, + correoElectronico: datosEmpleado.correoElectronico, + antiguedad: datosEmpleado.antiguedad?.format('YYYY-MM-DD'), + }; + + const erroresCampos = {}; + if (!datosProcesados.areaTrabajo || datosProcesados.areaTrabajo.trim() === '') { + erroresCampos.areaTrabajo = 'El campo no puede estar vacío ni contener sólo espacios'; + } + if (!datosProcesados.posicion || datosProcesados.posicion.trim() === '') { + erroresCampos.posicion = 'El campo no puede estar vacío ni contener sólo espacios'; + } + + const nuevosErrores = { + ...erroresCampos, + ...validarDatosActualizarEmpleado(datosProcesados), + }; + + if (Object.keys(nuevosErrores).length > 0) { + setErroresValidacion(nuevosErrores); + setAlerta({ + tipo: 'error', + mensaje: 'Corrige los errores en el formulario antes de guardar.', + }); + return { exito: false }; + } + + setErroresValidacion({}); + setAlerta(null); setCargando(true); - setError(null); - setMensaje(''); try { - const respuesta = await RepositorioActualizarEmpleado.actualizar(cambios); - setMensaje(respuesta.mensaje || 'Actualización exitosa'); - } catch (err) { - setError(err.message || 'Ocurrió un error'); + await RepositorioActualizarEmpleado.actualizar(datosProcesados); + setAlerta({ + tipo: 'success', + mensaje: esEdicion ? 'Empleado actualizado correctamente' : 'Empleado creado correctamente', + }); + return { + exito: true, + mensaje: esEdicion ? 'Empleado actualizado correctamente' : 'Empleado creado correctamente', + }; + } catch (error) { + setAlerta({ + tipo: 'error', + mensaje: error.message || 'Error al guardar el empleado', + }); + return { exito: false, mensaje: error.message || 'Error al guardar el empleado' }; } finally { setCargando(false); } }; - return { actualizar, cargando, error, mensaje }; -} + + const limpiarFormulario = () => { + setDatosEmpleado({ + ...datosEmpleado, + idEmpleado: null, + numeroEmergencia: '', + areaTrabajo: '', + posicion: '', + cantidadPuntos: '', + antiguedad: null, + }); + setErroresValidacion({}); + }; + + return { + datosEmpleado, + setDatosEmpleado, + erroresValidacion, + alerta, + setAlerta, + cargando, + esEdicion, + manejarCambio, + manejarAntiguedad, + obtenerHelperText, + handleGuardar, + limpiarFormulario, + CAMPO_OBLIGATORIO, + }; +}; diff --git a/src/hooks/Empleados/useActualizarGrupoEmpleados.js b/src/hooks/Empleados/useActualizarGrupoEmpleados.js new file mode 100644 index 00000000..a9c267ba --- /dev/null +++ b/src/hooks/Empleados/useActualizarGrupoEmpleados.js @@ -0,0 +1,67 @@ +import { useState } from 'react'; +import { RepositorioActualizarGrupoEmpleados } from '@Repositorios/Empleados/RepositorioActualizarGrupoEmpleados'; + +/** + * Hook para actualizar los datos de un grupo de empleados + * @returns {Object} Objeto con la función de actualización y estados + */ +export const useActualizarGrupoEmpleados = () => { + const [mensaje, setMensaje] = useState(''); + const [exito, setExito] = useState(false); + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(false); + + const actualizarGrupo = async (idGrupo, nombre, descripcion, empleados, setsDeProductos) => { + if (!idGrupo) return; + setCargando(true); + setExito(false); + setError(false); + setMensaje(''); + + try { + const resultado = await RepositorioActualizarGrupoEmpleados.actualizarGrupoEmpleados( + idGrupo, + nombre, + descripcion, + empleados, + setsDeProductos + ); + + // Consideramos la actualización exitosa si tenemos una respuesta del servidor + setExito(true); + setMensaje(resultado?.data?.mensaje || 'Grupo actualizado exitosamente'); + return resultado; + } catch (err) { + setExito(false); + setError(true); + const errorMessage + = err?.response?.data?.mensaje + || err?.response?.data?.error + || err?.message + || 'Ocurrió un error al actualizar el grupo de empleados'; + + setMensaje(errorMessage); + throw err; + } finally { + setCargando(false); + } + }; + + const resetEstado = () => { + setExito(false); + setError(false); + setMensaje(''); + }; + + return { + actualizarGrupo, + cargando, + exito, + error, + mensaje, + setError, + resetEstado, + }; +}; + +export default useActualizarGrupoEmpleados; diff --git a/src/hooks/Empleados/useCrearEmpleado.js b/src/hooks/Empleados/useCrearEmpleado.js new file mode 100644 index 00000000..1129347e --- /dev/null +++ b/src/hooks/Empleados/useCrearEmpleado.js @@ -0,0 +1,228 @@ +//RF16 - Crear empleado - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF16 +import { useState } from 'react'; +import { useAuth } from '@Hooks/AuthProvider'; +import { RepositorioCrearEmpleado } from '@Repositorios/Empleados/RepositorioCrearEmpleado'; + +// --- Validación robusta --- +const validarDatosCrearEmpleado = (datos) => { + const errores = {}; + const correoValido = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const telefonoValido = /^\d{10}$/; + const tieneCaracterEspecial = /[!@#$%^&*(),.?":{}|<>]/; + const tieneMayuscula = /[A-Z]/; + const tieneMinuscula = /[a-z]/; + const soloIngles = /^[A-Za-z0-9!@#$%^&*(),.?":{}|<> ]*$/; + const tieneNumero = /\d/; + + if (!datos.nombreCompleto || datos.nombreCompleto.trim() === '') { + errores.nombreCompleto = true; + } else if (datos.nombreCompleto.length < 3) { + errores.nombreCompleto = 'El nombre completo debe tener al menos 3 caracteres'; + } else if (!datos.nombreCompleto.includes(' ')) { + errores.nombreCompleto = 'Debe ingresar al menos un nombre y un apellido'; + } else if (datos.nombreCompleto.startsWith(' ') || datos.nombreCompleto.endsWith(' ')) { + errores.nombreCompleto = 'El nombre completo no debe tener espacios al inicio o al final'; + } else if (datos.nombreCompleto.includes(' ')) { + errores.nombreCompleto = 'El nombre completo no debe contener dos o más espacios seguidos'; + } + + if (!datos.fechaNacimiento) { + errores.fechaNacimiento = true; + } + + if (!datos.genero) errores.genero = true; + if (!datos.correoElectronico) { + errores.correoElectronico = true; + } else if (!correoValido.test(datos.correoElectronico)) { + errores.correoElectronico = 'Correo electrónico no válido'; + } + if (!datos.numeroTelefono) { + errores.numeroTelefono = true; + } else if (!telefonoValido.test(datos.numeroTelefono)) { + errores.numeroTelefono = 'El número de teléfono debe tener exactamente 10 dígitos'; + } + if (!datos.direccion || datos.direccion.trim() === '') { + errores.direccion = true; + } + if (!datos.contrasenia || datos.contrasenia.trim() === '') { + errores.contrasenia = true; + } else if (datos.contrasenia.length < 8) { + errores.contrasenia = 'La contraseña debe tener al menos 8 caracteres'; + } else if (!tieneCaracterEspecial.test(datos.contrasenia)) { + errores.contrasenia = 'Debe contener al menos un carácter especial'; + } else if (!soloIngles.test(datos.contrasenia)) { + errores.contrasenia = 'No se permiten ñ, Ñ ni tildes.'; + } else if (!tieneMayuscula.test(datos.contrasenia)) { + errores.contrasenia = 'Debe contener al menos una letra mayúscula'; + } else if (!tieneMinuscula.test(datos.contrasenia)) { + errores.contrasenia = 'Debe contener al menos una letra minúscula'; + } 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'; + } + + if (!datos.confirmarContrasenia || datos.confirmarContrasenia.trim() === '') { + errores.confirmarContrasenia = true; + } else if (datos.contrasenia !== datos.confirmarContrasenia) { + errores.confirmarContrasenia = 'Las contraseñas no coinciden'; + } + if (!datos.antiguedad) { + errores.antiguedad = true; + } + if (!datos.areaTrabajo || datos.areaTrabajo.trim() === '') { + errores.areaTrabajo = true; + } + if (!datos.posicion || datos.posicion.trim() === '') { + errores.posicion = true; + } + if ( + !datos.cantidadPuntos || + isNaN(Number(datos.cantidadPuntos)) || + Number(datos.cantidadPuntos) < 0 + ) { + errores.cantidadPuntos = 'La cantidad de puntos debe ser un número positivo'; + } + if (!datos.numeroEmergencia || !telefonoValido.test(datos.numeroEmergencia)) { + errores.numeroEmergencia = !datos.numeroEmergencia + ? true + : 'El número de emergencia debe tener exactamente 10 dígitos'; + } + return errores; +}; + +export const useCrearEmpleado = () => { + const { usuario } = useAuth(); + const idCliente = usuario?.clienteSeleccionado?.idCliente || usuario?.clienteSeleccionado; + + const estadoInicial = { + nombreCompleto: '', + correoElectronico: '', + contrasenia: '', + confirmarContrasenia: '', + numeroTelefono: '', + direccion: '', + fechaNacimiento: null, + numeroEmergencia: '', + genero: '', + areaTrabajo: '', + posicion: '', + cantidadPuntos: '', + antiguedad: null, + estatus: '1', + idCliente, + idRol: '3', + }; + + const [datosEmpleado, setDatosEmpleado] = useState(estadoInicial); + const [erroresValidacion, setErroresValidacion] = useState({}); + const [cargando, setCargando] = useState(false); + const [alerta, setAlerta] = useState(null); + + const CAMPO_OBLIGATORIO = 'Este campo es obligatorio'; + + const manejarCambio = (evento) => { + const { name, value } = evento.target; + let nuevoValor = value; + + // Solo números para ciertos campos + if (['numeroEmergencia', 'numeroTelefono', 'cantidadPuntos'].includes(name)) { + nuevoValor = value.replace(/\D/g, '').slice(0, 10); + } + + setDatosEmpleado((prev) => ({ + ...prev, + [name]: nuevoValor, + })); + }; + + const manejarAntiguedad = (nuevaFecha) => { + setDatosEmpleado((prev) => ({ + ...prev, + antiguedad: nuevaFecha, + })); + }; + + const manejarFechaNacimiento = (nuevaFecha) => { + setDatosEmpleado((prev) => ({ + ...prev, + fechaNacimiento: nuevaFecha, + })); + }; + + const obtenerHelperText = (campo) => { + const err = erroresValidacion[campo]; + if (err) { + return typeof err === 'string' ? err : CAMPO_OBLIGATORIO; + } + return ''; + }; + + const handleGuardar = async () => { + const datosProcesados = { + ...datosEmpleado, + antiguedad: datosEmpleado.antiguedad?.format?.('YYYY-MM-DD') || datosEmpleado.antiguedad, + fechaNacimiento: + datosEmpleado.fechaNacimiento?.format?.('YYYY-MM-DD') || datosEmpleado.fechaNacimiento, + }; + + const errores = validarDatosCrearEmpleado(datosProcesados) || {}; + + if (Object.keys(errores).length > 0) { + setErroresValidacion(errores); + setAlerta({ + tipo: 'error', + mensaje: 'Corrige los errores en el formulario antes de guardar.', + }); + return { exito: false }; + } + + setErroresValidacion({}); + setAlerta(null); + setCargando(true); + + try { + const respuesta = await RepositorioCrearEmpleado.crear(datosProcesados); + setAlerta({ + tipo: 'success', + mensaje: 'Empleado creado correctamente', + }); + limpiarFormulario(); + return { + exito: true, + mensaje: 'Empleado creado correctamente', + datos: respuesta, + }; + } catch (error) { + setAlerta({ + tipo: 'error', + mensaje: error.message || 'Error al guardar el empleado', + }); + return { exito: false, mensaje: error.message || 'Error al guardar el empleado' }; + } finally { + setCargando(false); + } + }; + + const limpiarFormulario = () => { + setDatosEmpleado(estadoInicial); + setErroresValidacion({}); + }; + + return { + datosEmpleado, + setDatosEmpleado, + erroresValidacion, + alerta, + setAlerta, + cargando, + manejarCambio, + manejarAntiguedad, + manejarFechaNacimiento, + obtenerHelperText, + handleGuardar, + limpiarFormulario, + CAMPO_OBLIGATORIO, + }; +}; diff --git a/src/hooks/Empleados/useExportarEmpleados.js b/src/hooks/Empleados/useExportarEmpleados.js new file mode 100644 index 00000000..ed01a803 --- /dev/null +++ b/src/hooks/Empleados/useExportarEmpleados.js @@ -0,0 +1,42 @@ +import { useState } from 'react'; +import { exportarEmpleados as repoExportarEmpleados } from '@Repositorios/Empleados/RepositorioExportarEmpleados'; + +/** + * Hook para gestionar la exportación de empleados en formato CSV. + * + * @returns {{ + * exportar: (idsEmpleado: number[]) => Promise, + * cargando: boolean, + * error: string | null, + * csv: string, + * mensaje: string + * }} + * @see [RF59 - Exportar Empleados](https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF59) + */ +const useExportarEmpleados = () => { + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(null); + const [csv, setCsv] = useState(''); + const [mensaje, setMensaje] = useState(''); + + const exportar = async (idsEmpleado) => { + setCargando(true); + setError(null); + setCsv(''); + setMensaje(''); + + try { + const { mensaje, csv } = await repoExportarEmpleados(idsEmpleado); + setMensaje(mensaje); + setCsv(csv); + } catch (err) { + setError(err.message); + } finally { + setCargando(false); + } + }; + + return { exportar, cargando, error, csv, mensaje }; +}; + +export default useExportarEmpleados; \ No newline at end of file diff --git a/src/hooks/Eventos/useCrearEvento.js b/src/hooks/Eventos/useCrearEvento.js new file mode 100644 index 00000000..68f155a5 --- /dev/null +++ b/src/hooks/Eventos/useCrearEvento.js @@ -0,0 +1,51 @@ +// RF36 - Crear Evento - [https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF36] + +import { useState } from 'react'; +import { RepositorioCrearEvento } from '@Dominio/Repositorios/Eventos/RepositorioCrearEvento'; +import { Evento } from '@Dominio/Modelos/Eventos/Eventos'; + +/** + * Hook para crear eventos. + * @return {{ + * crear: function, + * mensaje: string, + * cargando: boolean, + * error: string | null, + * resetError: function, + * }} + */ +export function useCrearEvento() { + const [mensaje, setMensaje] = useState(''); + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(null); + + /** + * Resetea el estado de error + */ + const resetError = () => setError(null); + + /** + * Crea un nuevo evento. + * + * @param {Evento} evento - Objeto evento a crear. + * @returns {Promise} - true si se creó correctamente, false si ocurrió un error + */ + const crear = async (evento) => { + setCargando(true); + setError(null); + + try { + const { mensaje } = await RepositorioCrearEvento.crearEvento(evento); + setMensaje(mensaje); + return true; // Indica éxito + } catch (err) { + setMensaje(''); + setError(err.message); + throw err; // Propaga el error para que el componente pueda manejarlo + } finally { + setCargando(false); + } + }; + + return { crear, mensaje, cargando, error, resetError }; +} \ No newline at end of file diff --git a/src/hooks/Pedidos/useActualizarPedido.js b/src/hooks/Pedidos/useActualizarPedido.js new file mode 100644 index 00000000..bd9ee0d4 --- /dev/null +++ b/src/hooks/Pedidos/useActualizarPedido.js @@ -0,0 +1,43 @@ +import { useState } from 'react'; +import { RepositorioActualizarPedido } from '@Dominio/Repositorios/Pedidos/repositorioActualizarPedido'; + +export const useActualizarPedido = () => { + const [mensaje, setMensaje] = useState(''); + const [exito, setExito] = useState(false); + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(false); + const actualizarPedido = async (pedido) => { + if (!pedido || !pedido.idPedido) { + setError(true); + setMensaje('Datos del pedido incompletos'); + return { exito: false, mensaje: 'Datos del pedido incompletos' }; + } + + setCargando(true); + setExito(false); + setError(false); + setMensaje(''); + try { + const respuesta = await RepositorioActualizarPedido.actualizarPedido(pedido); + setExito(true); + setMensaje(respuesta.data.mensaje || 'Pedido actualizado correctamente'); + return { + exito: true, + mensaje: respuesta.data.mensaje || 'Pedido actualizado correctamente', + data: respuesta, + }; + } catch (error) { + setExito(false); + setError(true); + const mensajeError = error.message || 'Error al actualizar el pedido'; + setMensaje(mensajeError); + return { exito: false, mensaje: mensajeError }; + } finally { + setCargando(false); + } + }; + + return { actualizarPedido, cargando, exito, error, mensaje, setError }; +}; + +export default useActualizarPedido; diff --git a/src/hooks/Productos/ProductoFormProvider.jsx b/src/hooks/Productos/ProductoFormProvider.jsx index 6526b713..6797f8f1 100644 --- a/src/hooks/Productos/ProductoFormProvider.jsx +++ b/src/hooks/Productos/ProductoFormProvider.jsx @@ -121,8 +121,8 @@ export const ProductoFormProvider = ({ children, alCerrarFormularioProducto }) = [campo]: valor, }, }; - }, []); - }); + }); + }, []); const manejarEliminarVariante = useCallback((idVariante) => { setVariantes((prev) => { diff --git a/src/hooks/Productos/useExportarProductos.js b/src/hooks/Productos/useExportarProductos.js new file mode 100644 index 00000000..ae7fd9dc --- /dev/null +++ b/src/hooks/Productos/useExportarProductos.js @@ -0,0 +1,62 @@ +// RF58 - Exportar Productos - https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF58 + +import { useState } from 'react'; +import { exportarProductos as repoExportarProductos } from '@Repositorios/Productos/RepositorioExportarProducto'; + +/** + * Hook para gestionar la exportación de productos en formato XLSX. + * + * @returns {{ + * exportar: (idsProductos: number[]) => Promise, + * cargando: boolean, + * error: string | null, + * mensaje: string + * }} + */ +const useExportarProductos = () => { + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(null); + const [mensaje, setMensaje] = useState(''); + + const exportar = async (idsProducto) => { + setCargando(true); + setError(null); + setMensaje(''); + + try { + const respuesta = await repoExportarProductos(idsProducto); + + const fecha = new Date() + .toLocaleDateString('es-CO', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + .split('/') + .reverse() + .join('-'); + const url = window.URL.createObjectURL( + new Blob([respuesta], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }) + ); + + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `productos_${fecha}.xlsx`); + document.body.appendChild(link); + link.click(); + link.remove(); + + setMensaje('Archivo exportado exitosamente'); + } catch (err) { + setError(err.message); + } finally { + setCargando(false); + } + }; + + return { exportar, cargando, error, mensaje }; +}; + +export default useExportarProductos; diff --git a/src/hooks/Productos/useImportarProductos.js b/src/hooks/Productos/useImportarProductos.js new file mode 100644 index 00000000..eae0460c --- /dev/null +++ b/src/hooks/Productos/useImportarProductos.js @@ -0,0 +1,50 @@ +import { useState, useEffect } from 'react'; +import { RepositorioImportarProductos } from '@Repositorios/Productos/RepositorioImportarProductos'; + +/** + * Hook para importar productos masivamente desde un CSV (sin imágenes). + * + * @returns {{ + * importar: (productosParseados: Array) => Promise, + * cargando: boolean, + * errores: Array<{ fila: number|string, error: string }>, + * exito: boolean + * }} + */ +const useImportarProductos = () => { + const [cargando, setCargando] = useState(false); + const [errores, setErrores] = useState([]); + const [exito, setExito] = useState(false); + + const importar = async (productosParseados) => { + setCargando(true); + setErrores([]); + setExito(false); + + try { + + const respuesta = await RepositorioImportarProductos.importarProductos(productosParseados); + + if (respuesta.errores) { + setErrores(respuesta.errores); + } else { + setExito(true); + } + } catch (err) { + setErrores([{ fila: '-', error: err.message }]); + } finally { + setCargando(false); + } + }; + + useEffect(() => { + if (exito) { + const timer = setTimeout(() => setExito(false), 0); + return () => clearTimeout(timer); + } + }, [exito]); + + return { importar, cargando, errores, exito }; +}; + +export default useImportarProductos; diff --git a/src/hooks/Productos/useLeerProducto.js b/src/hooks/Productos/useLeerProducto.js new file mode 100644 index 00000000..f3b146d2 --- /dev/null +++ b/src/hooks/Productos/useLeerProducto.js @@ -0,0 +1,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) + + useEffect(() => { + const obtenerInfoProducto = async () => { + setCargando(true) + setError(null) + + try{ + const productoInfo = await RepositorioLeerProducto.obtenerPorId(idProducto); + setDetalleProducto(productoInfo) + }catch (err) { + setError(err.message) + }finally { + setCargando(false) + } + } + + if(idProducto) { + obtenerInfoProducto() + } + }, [idProducto]); + + return {detalleProducto, cargando, error} +} \ No newline at end of file diff --git a/src/hooks/Roles/useActualizarRol.js b/src/hooks/Roles/useActualizarRol.js new file mode 100644 index 00000000..6f1c403c --- /dev/null +++ b/src/hooks/Roles/useActualizarRol.js @@ -0,0 +1,50 @@ +//RF[8] Leer Rol - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF8 + +import { useState } from 'react'; +import { RepositorioActualizarRol } from '@Repositorios/Roles/RepositorioActualizarRol'; + +const useActualizarRol = () => { + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(null); + const [exitoso, setExitoso] = useState(false); + const [mensaje, setMensaje] = useState(''); + + const actualizarRol = async (idRol, datosRol) => { + setCargando(true); + setError(null); + setExitoso(false); + setMensaje(''); + + try { + const respuesta = await RepositorioActualizarRol.actualizar(idRol, datosRol); + setMensaje(respuesta.mensaje || 'Rol actualizado exitosamente'); + setExitoso(true); + return { success: true, data: respuesta }; + + } catch (err) { + const mensajeError = err.message || 'Error al actualizar el rol'; + setError(mensajeError); + console.error('Error al actualizar rol:', err); + return { success: false, error: mensajeError }; + } finally { + setCargando(false); + } + }; + + const limpiarEstado = () => { + setError(null); + setExitoso(false); + setMensaje(''); + }; + + return { + actualizarRol, + cargando, + error, + exitoso, + mensaje, + limpiarEstado, + }; +}; + +export default useActualizarRol; \ No newline at end of file diff --git a/src/hooks/Roles/useLeerRol.js b/src/hooks/Roles/useLeerRol.js new file mode 100644 index 00000000..e7f2ec0e --- /dev/null +++ b/src/hooks/Roles/useLeerRol.js @@ -0,0 +1,27 @@ +import { useState, useCallback } from 'react'; +import { RepositorioLeerRol } from '@Repositorios/Roles/RepositorioLeerRol'; + +/** + * Hook para leer el detalle de un rol. + */ +export function useLeerRol() { + const [detalle, setDetalle] = useState(null); + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(null); + + const leerRol = useCallback(async (idRol) => { + setCargando(true); + setError(null); + + try { + const detalleRol = await RepositorioLeerRol.obtenerDetalle(idRol); + setDetalle(detalleRol); + } catch (err) { + setError(err.message || 'Error al leer el rol'); + } finally { + setCargando(false); + } + }, []); + + return { detalle, cargando, error, leerRol }; +} \ No newline at end of file diff --git a/src/hooks/SetsProductos/useActualizarSetsProductos.js b/src/hooks/SetsProductos/useActualizarSetsProductos.js new file mode 100644 index 00000000..d620d583 --- /dev/null +++ b/src/hooks/SetsProductos/useActualizarSetsProductos.js @@ -0,0 +1,69 @@ +// RF44 - Actualiza Set de Productos - https://codeandco-wiki.netlify.app/docs/next/proyectos/textiles/documentacion/requisitos/RF44 + +import { useState } from 'react'; +import { RepositorioActualizarSetProductos } from '@Repositorios/SetsProductos/RepositorioActualizarSetProductos'; + +/** + * Hook para actualizar los datos de un set de productos + * @returns {Object} Objeto con la función de actualización y estados + */ +export const useActualizarSetsProductos = () => { + const [mensaje, setMensaje] = useState(''); + const [exito, setExito] = useState(false); + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(false); + + const actualizarSet = async (idSet, nombre, descripcion, activo, productos) => { + if (!idSet) return; + + setCargando(true); + setExito(false); + setError(false); + setMensaje(''); + + try { + const resultado = await RepositorioActualizarSetProductos.actualizarSetProductos( + idSet, + nombre, + descripcion, + activo, + productos + ); + + setExito(true); + setMensaje(resultado?.data?.mensaje || 'Set de productos actualizado exitosamente'); + return resultado; + } catch (err) { + setExito(false); + setError(true); + const errorMessage + = err?.response?.data?.mensaje + || err?.response?.data?.error + || err?.message + || 'Ocurrió un error al actualizar el set de productos'; + + setMensaje(errorMessage); + throw err; + } finally { + setCargando(false); + } + }; + + const resetEstado = () => { + setExito(false); + setError(false); + setMensaje(''); + }; + + return { + actualizarSet, + cargando, + exito, + error, + mensaje, + setError, + resetEstado, + }; +}; + +export default useActualizarSetsProductos; diff --git a/src/hooks/SetsProductos/useConsultarSetsProductos.js b/src/hooks/SetsProductos/useConsultarSetsProductos.js index 0bbec6ab..31047e0d 100644 --- a/src/hooks/SetsProductos/useConsultarSetsProductos.js +++ b/src/hooks/SetsProductos/useConsultarSetsProductos.js @@ -32,7 +32,6 @@ export function useConsultarSetsProductos() { try { const { setsDeProductos, mensaje } = await RepositorioConsultarSetsProductos.obtenerLista(); - setSetsProductos(setsDeProductos); setMensaje(mensaje); } catch (err) { diff --git a/src/hooks/SetsProductos/useCrearSetsProducto.js b/src/hooks/SetsProductos/useCrearSetsProducto.js new file mode 100644 index 00000000..02f0439a --- /dev/null +++ b/src/hooks/SetsProductos/useCrearSetsProducto.js @@ -0,0 +1,74 @@ +import { useState, useCallback } from 'react'; +import { RepositorioCrearSetsProducto } from '@Repositorios/SetsProductos/RepositorioCrearSetsProducto.js'; +import { CrearSetsProducto } from '@Modelos/SetsProductos/CrearSetsProductos'; + +const useCrearSetsProducto = () => { + const [cargando, setCargando] = useState(false); + const [exito, setExito] = useState(false); + const [error, setError] = useState(false); + const [mensaje, setMensaje] = useState(''); + + const crearSetsProducto = useCallback(async ({ nombre, nombreVisible, descripcion, productos }) => { + setCargando(true); + setExito(false); + setError(false); + setMensaje(''); + + try { + const productosTransformados = productos.map((producto) => ({ + idProducto: producto.id, + })); + + const nuevoSetsProducto = new CrearSetsProducto({ + nombre, + nombreVisible, + descripcion, + productos: productosTransformados, + }); + + + const resultado = await RepositorioCrearSetsProducto.crearSetsProducto(nuevoSetsProducto); + + if (resultado.data.mensaje) { + setExito(true); + setMensaje(resultado.data.mensaje); + return resultado; + } else { + throw new Error(resultado.data?.mensaje || 'Error desconocido'); + } + } catch (err) { + setExito(false); + setError(true); + + if (err.response?.data?.error) { + setMensaje(err.response.data.error); + } else if (err instanceof Error) { + setMensaje(err.message); + } else { + setMensaje('Ocurrió un error al crear el set de productos'); + } + + return false; + } finally { + setCargando(false); + } + }, []); + + const resetEstado = () => { + setExito(false); + setError(false); + setMensaje(''); + }; + + return { + crearSetsProducto, + cargando, + exito, + error, + mensaje, + setError, + resetEstado, + }; +}; + +export default useCrearSetsProducto; \ No newline at end of file diff --git a/src/hooks/Usuarios/useAccionesUsuario.js b/src/hooks/Usuarios/useAccionesUsuario.js new file mode 100644 index 00000000..ed400e03 --- /dev/null +++ b/src/hooks/Usuarios/useAccionesUsuario.js @@ -0,0 +1,176 @@ +import { useState } from 'react'; +import dayjs from 'dayjs'; +import { RepositorioActualizarUsuario } from '@Repositorios/Usuarios/RepositorioActualizarUsuario'; +import { validarDatosActualizarUsuario } from '@Modelos/Usuarios/ActualizarUsuario'; + +export const useAccionesUsuario = (usuarioInicial = null) => { + const esEdicion = !!usuarioInicial; + const [erroresValidacion, setErroresValidacion] = useState({}); + const [cargando, setCargando] = useState(false); + const [alerta, setAlerta] = useState(null); + + const [datosUsuario, setDatosUsuario] = useState(() => { + if (esEdicion) { + let fechaNacimiento = null; + if (usuarioInicial.fechaNacimiento) { + fechaNacimiento = dayjs(usuarioInicial.fechaNacimiento); + } + + let genero = ''; + if (usuarioInicial.genero) { + const generoLower = usuarioInicial.genero.toLowerCase(); + if (['hombre', 'masculino'].includes(generoLower)) genero = 'Hombre'; + else if (['mujer', 'femenino'].includes(generoLower)) genero = 'Mujer'; + else if (['otro', 'no binario', 'nb'].includes(generoLower)) genero = 'Otro'; + else genero = 'Otro'; + } + + let idRol = usuarioInicial.idRol ?? ''; + if (typeof usuarioInicial.rol === 'string') { + const rolesMap = { + 'Super Administrador': 1, + Administrador: 2, + }; + idRol = rolesMap[usuarioInicial.rol] || ''; + } else if (usuarioInicial.rol) { + idRol = usuarioInicial.rol; + } + + return { + nombreCompleto: usuarioInicial.nombreCompleto ?? '', + apellido: usuarioInicial.apellido ?? '', + correoElectronico: usuarioInicial.correoElectronico ?? '', + contrasenia: '', + confirmarContrasenia: '', + numeroTelefono: usuarioInicial.numeroTelefono ?? '', + direccion: usuarioInicial.direccion ?? '', + fechaNacimiento, + genero, + idRol: idRol ?? '', + cliente: Array.isArray(usuarioInicial.cliente) + ? usuarioInicial.cliente + : usuarioInicial.idCliente + ? [usuarioInicial.idCliente] + : typeof usuarioInicial.cliente === 'number' + ? [usuarioInicial.cliente] + : [], + estatus: usuarioInicial.estatus !== undefined ? usuarioInicial.estatus : 1, + }; + } + return { + nombreCompleto: '', + apellido: '', + correoElectronico: '', + contrasenia: '', + confirmarContrasenia: '', + numeroTelefono: '', + direccion: '', + fechaNacimiento: null, + genero: '', + idRol: '', + cliente: [], + estatus: 1, + }; + }); + + const CAMPO_OBLIGATORIO = 'Este campo es obligatorio'; + + // Métodos para manejar cambios + const manejarCambio = (evento) => { + const { name, value } = evento.target; + setDatosUsuario((prev) => ({ ...prev, [name]: value })); + }; + + const manejarFechaNacimiento = (nuevaFecha) => { + setDatosUsuario((prev) => ({ + ...prev, + fechaNacimiento: nuevaFecha, + })); + }; + + const obtenerHelperText = (campo) => { + const err = erroresValidacion[campo]; + if (err) { + return typeof err === 'string' ? err : CAMPO_OBLIGATORIO; + } + return ''; + }; + + const handleGuardar = async () => { + const datosProcesados = { + ...datosUsuario, + idUsuario: esEdicion ? usuarioInicial.idUsuario || usuarioInicial.id : datosUsuario.idUsuario, + fechaNacimiento: datosUsuario.fechaNacimiento + ? dayjs(datosUsuario.fechaNacimiento).format('YYYY-MM-DD') + : null, + nombreCompleto: datosUsuario.nombreCompleto, + apellido: datosUsuario.apellido, + }; + + const nuevosErrores = validarDatosActualizarUsuario(datosProcesados); + + if (Object.keys(nuevosErrores).length > 0) { + setErroresValidacion(nuevosErrores); + return { exito: false }; + } + + setErroresValidacion({}); + setAlerta(null); + setCargando(true); + + try { + const respuesta = await RepositorioActualizarUsuario.actualizar(datosProcesados); + setAlerta({ + tipo: 'success', + mensaje: respuesta?.mensaje || 'Usuario actualizado correctamente', + }); + return { + exito: true, + mensaje: respuesta?.mensaje || (esEdicion ? 'Usuario actualizado correctamente' : ''), + }; + } catch (error) { + setAlerta({ + tipo: 'error', + mensaje: error?.message || 'Error al guardar el usuario', + }); + return { + exito: false, + mensaje: error?.message || 'Error al guardar el usuario', + }; + } finally { + setCargando(false); + } + }; + + const limpiarFormulario = () => { + setDatosUsuario({ + nombreCompleto: '', + correoElectronico: '', + contrasenia: '', + numeroTelefono: '', + direccion: '', + fechaNacimiento: null, + genero: '', + idrol: '', + cliente: [], + }); + setErroresValidacion({}); + setAlerta(null); + }; + + return { + datosUsuario, + setDatosUsuario, + erroresValidacion, + alerta, + setAlerta, + cargando, + esEdicion, + manejarCambio, + manejarFechaNacimiento, + obtenerHelperText, + handleGuardar, + limpiarFormulario, + CAMPO_OBLIGATORIO, + }; +}; diff --git a/src/hooks/Usuarios/useActualizarUsuario.js b/src/hooks/Usuarios/useActualizarUsuario.js new file mode 100644 index 00000000..288d88bc --- /dev/null +++ b/src/hooks/Usuarios/useActualizarUsuario.js @@ -0,0 +1,25 @@ +import { useState } from 'react'; +import { RepositorioActualizarUsuario } from '@Repositorios/Usuarios/RepositorioActualizarUsuario'; + +export function useActualizarUsuario() { + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(null); + const [mensaje, setMensaje] = useState(''); + + const actualizar = async (cambios) => { + setCargando(true); + setError(null); + setMensaje(''); + + try { + const respuesta = await RepositorioActualizarUsuario.actualizar(cambios); + setMensaje(respuesta.mensaje || 'Actualización exitosa'); + } catch (err) { + setError(err.message || 'Ocurrió un error'); + } finally { + setCargando(false); + } + }; + + return { actualizar, cargando, error, mensaje }; +}