From 41930a5c23ee4e59bf6155ae86e724d43e633201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Antonio=20Ben=C3=ADtez=20De=20La=20Portilla?= Date: Sun, 11 May 2025 04:03:10 -0600 Subject: [PATCH 001/160] feat: add LEER_CUOTAS route to constantes for cuotas --- src/Utilidades/Constantes/rutas.js | 6 +++++- src/Utilidades/Constantes/rutasAPI.js | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Utilidades/Constantes/rutas.js b/src/Utilidades/Constantes/rutas.js index 8f7ba85e..5d0085ae 100644 --- a/src/Utilidades/Constantes/rutas.js +++ b/src/Utilidades/Constantes/rutas.js @@ -29,7 +29,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}`, diff --git a/src/Utilidades/Constantes/rutasAPI.js b/src/Utilidades/Constantes/rutasAPI.js index 29c64135..cb846f11 100644 --- a/src/Utilidades/Constantes/rutasAPI.js +++ b/src/Utilidades/Constantes/rutasAPI.js @@ -52,6 +52,7 @@ export const RUTAS_API = { 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`, }, ROLES: { BASE: BASE_ROLES, From c4a92e8bd25fed1b0545d9ddc402b7689c7ed99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Antonio=20Ben=C3=ADtez=20De=20La=20Portilla?= Date: Sun, 11 May 2025 23:00:49 -0600 Subject: [PATCH 002/160] =?UTF-8?q?feat:=20modelo,=20repositorio=20y=20hoo?= =?UTF-8?q?k=20de=20leerCuota=20funciona=20bien=20yno=20di=C3=B3=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Dominio/Modelos/Cuotas/LeerCuota.js | 22 +++++++++ .../Cuotas/repositorioLeerCuota.js | 41 +++++++++++++++++ src/hooks/Cuotas/useLeerCuota.js | 46 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/Dominio/Modelos/Cuotas/LeerCuota.js create mode 100644 src/Dominio/Repositorios/Cuotas/repositorioLeerCuota.js create mode 100644 src/hooks/Cuotas/useLeerCuota.js diff --git a/src/Dominio/Modelos/Cuotas/LeerCuota.js b/src/Dominio/Modelos/Cuotas/LeerCuota.js new file mode 100644 index 00000000..c60f178f --- /dev/null +++ b/src/Dominio/Modelos/Cuotas/LeerCuota.js @@ -0,0 +1,22 @@ +/* +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, + periodoRenovacion, + renovacionHabilitada, + ultimaActualizacion, + }) { + this.idCuota = idCuota; + this.nombre = nombre; + this.descripcion = descripcion; + this.periodoRenovacion = periodoRenovacion; + this.renovacionHabilitada = renovacionHabilitada; + this.ultimaActualizacion = ultimaActualizacion; + } +} diff --git a/src/Dominio/Repositorios/Cuotas/repositorioLeerCuota.js b/src/Dominio/Repositorios/Cuotas/repositorioLeerCuota.js new file mode 100644 index 00000000..4027d79e --- /dev/null +++ b/src/Dominio/Repositorios/Cuotas/repositorioLeerCuota.js @@ -0,0 +1,41 @@ +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(idCuota) { + try { + const respuesta = await axios.post( + RUTAS_API.CUOTAS.CONSULTAR_CUOTA, + { idCuota }, + { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + } + ); + + const { cuota, mensaje } = respuesta.data; + + const cuotaInstancia = new LeerCuota(cuota); + + return { + cuota: cuotaInstancia, + mensaje, + }; + } catch (error) { + const mensaje = error.response?.data?.mensaje || 'Error al obtener datos de la cuota.'; + throw new Error(mensaje); + } + } +} 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 }; +}; From 9326c4f4bf5e542cbaf6979402ef1a81d8db0746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Antonio=20Ben=C3=ADtez=20De=20La=20Portilla?= Date: Sun, 11 May 2025 23:39:08 -0600 Subject: [PATCH 003/160] =?UTF-8?q?feat:=20agregar=20rutas=20y=20permisos?= =?UTF-8?q?=20a=20la=20p=C3=A1gina=20de=20ListaCuotas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Vistas/Paginas/Cuotas/ListaCuotas.jsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx index e8bcab15..ba2cbf32 100644 --- a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx +++ b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx @@ -13,7 +13,9 @@ import Alerta from '@Moleculas/Alerta'; 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'; const ListaCuotas = () => { const navegar = useNavigate(); const { usuario } = useAuth(); @@ -137,7 +139,15 @@ const ListaCuotas = () => { descripcion='Consulta y administra los sets de cuotas registrados para cada cliente.' informacionBotones={botones} > - + {error && } From a48325f58e5013bc5c705813283f74754ecdf926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Antonio=20Ben=C3=ADtez=20De=20La=20Portilla?= Date: Mon, 12 May 2025 20:47:07 -0600 Subject: [PATCH 004/160] Restaurando el commit eliminado --- package-lock.json | 37 -- package.json | 2 - src/Dominio/Modelos/Cuotas/CuotaSetModelo.js | 4 +- .../Empleados/GrupoEmpleadosLectura.js | 14 + src/Dominio/Modelos/Pagos/ListaTipoPago.js | 12 + src/Dominio/Modelos/Pagos/TipoPago.js | 9 + .../Modelos/SetsProductos/SetProductos.js | 5 +- .../Clientes/repositorioActualizarCliente.js | 23 ++ .../RepositorioLeerGrupoEmpleados.js | 42 ++ .../Pagos/RepositorioActualizarTipoPago.js | 27 ++ .../Pagos/RepositorioConsultarTipoPagos.js | 24 ++ .../Productos/repositorioEliminarProductos.js | 5 +- src/Dominio/Servicios/obtenerPermisos.js | 5 +- src/Rutas/RutasSistemaAdministrativo.jsx | 7 +- src/Utilidades/Constantes/rutas.js | 3 +- src/Utilidades/Constantes/rutasAPI.js | 8 + .../Componentes/Moleculas/ClienteInfo.jsx | 117 ------ .../Componentes/Moleculas/CuotasInfo.jsx | 70 ++++ .../Moleculas/GrupoEmpleadosInfo.jsx | 95 +++++ .../Componentes/Moleculas/InfoCliente.jsx | 267 +++++++++++++ .../{UsuarioInfo.jsx => InfoUsuario.jsx} | 0 .../Moleculas/SetProductosInfo.jsx | 78 ++++ .../Moleculas/TarjetaConImagen.jsx | 26 +- .../Clientes/AgregarClienteTarjeta.jsx | 47 +++ .../Organismos/Clientes/ClienteTarjeta.jsx | 26 ++ .../Organismos/Clientes/ClientesLista.jsx | 37 ++ .../Clientes/DetalleClienteModal.jsx | 103 +++++ .../Clientes/EliminarClientesModal.jsx | 44 +++ .../{ => Clientes}/ModalLeerCliente.jsx | 5 + .../{ => Cuotas}/FormaCrearCuotaSet.jsx | 2 + .../{ => Cuotas}/ModalCrearCuotaSet.jsx | 2 + .../Formularios/FormaCrearCuotaSet.jsx | 2 + .../Componentes/Organismos/ModalFlotante.jsx | 6 +- .../Organismos/TarjetaConfiguracionPagos.jsx | 196 +++++++++ src/Vistas/Paginas/Clientes/ListaClientes.jsx | 327 ++++----------- .../Configuracion/ConfiguracionGeneral.jsx | 11 +- src/Vistas/Paginas/Cuotas/EditarCuotas.jsx | 2 + src/Vistas/Paginas/Cuotas/ListaCuotas.jsx | 30 +- .../Paginas/Empleados/ListaEmpleados.jsx | 4 +- .../Paginas/Empleados/ListaGrupoEmpleados.jsx | 65 ++- src/Vistas/Paginas/Pedidos/ListaPedidos.jsx | 10 +- .../Paginas/Productos/ListaProductos.jsx | 9 +- .../SetsProductos/ListaSetsProductos.jsx | 65 ++- src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx | 2 +- src/Vistas/stories/ClienteInfo.stories.js | 10 +- src/Vistas/stories/ModalFlotante.stories.jsx | 2 +- .../stories/SetProductosInfo.stories.js | 29 ++ src/Vistas/stories/UsuarioInfo.stories.js | 24 +- src/hooks/Clientes/useClientes.js | 372 ++++++++++++++++++ src/hooks/Clientes/useLeerCliente.js | 16 +- src/hooks/Cuotas/useCrearCuotaSet.js | 3 +- src/hooks/Empleados/useLeerGrupoEmpleados.js | 47 +++ src/hooks/Pagos/useActualizarTipoPago.js | 27 ++ src/hooks/Pagos/useConsultarTipoPagos.js | 63 +++ src/hooks/Productos/useEliminarProductos.js | 4 +- src/hooks/Roles/useEliminarRol.js | 1 - src/theme.js | 2 + 57 files changed, 2000 insertions(+), 475 deletions(-) create mode 100644 src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js create mode 100644 src/Dominio/Modelos/Pagos/ListaTipoPago.js create mode 100644 src/Dominio/Modelos/Pagos/TipoPago.js create mode 100644 src/Dominio/Repositorios/Clientes/repositorioActualizarCliente.js create mode 100644 src/Dominio/Repositorios/Empleados/RepositorioLeerGrupoEmpleados.js create mode 100644 src/Dominio/Repositorios/Pagos/RepositorioActualizarTipoPago.js create mode 100644 src/Dominio/Repositorios/Pagos/RepositorioConsultarTipoPagos.js delete mode 100644 src/Vistas/Componentes/Moleculas/ClienteInfo.jsx create mode 100644 src/Vistas/Componentes/Moleculas/CuotasInfo.jsx create mode 100644 src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfo.jsx create mode 100644 src/Vistas/Componentes/Moleculas/InfoCliente.jsx rename src/Vistas/Componentes/Moleculas/{UsuarioInfo.jsx => InfoUsuario.jsx} (100%) create mode 100644 src/Vistas/Componentes/Moleculas/SetProductosInfo.jsx create mode 100644 src/Vistas/Componentes/Organismos/Clientes/AgregarClienteTarjeta.jsx create mode 100644 src/Vistas/Componentes/Organismos/Clientes/ClienteTarjeta.jsx create mode 100644 src/Vistas/Componentes/Organismos/Clientes/ClientesLista.jsx create mode 100644 src/Vistas/Componentes/Organismos/Clientes/DetalleClienteModal.jsx create mode 100644 src/Vistas/Componentes/Organismos/Clientes/EliminarClientesModal.jsx rename src/Vistas/Componentes/Organismos/{ => Clientes}/ModalLeerCliente.jsx (92%) rename src/Vistas/Componentes/Organismos/{ => Cuotas}/FormaCrearCuotaSet.jsx (94%) rename src/Vistas/Componentes/Organismos/{ => Cuotas}/ModalCrearCuotaSet.jsx (95%) create mode 100644 src/Vistas/Componentes/Organismos/TarjetaConfiguracionPagos.jsx create mode 100644 src/Vistas/stories/SetProductosInfo.stories.js create mode 100644 src/hooks/Clientes/useClientes.js create mode 100644 src/hooks/Empleados/useLeerGrupoEmpleados.js create mode 100644 src/hooks/Pagos/useActualizarTipoPago.js create mode 100644 src/hooks/Pagos/useConsultarTipoPagos.js diff --git a/package-lock.json b/package-lock.json index 8cd02843..d5b2b93e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,6 @@ "react-dom": "^19.0.0", "react-pro-sidebar": "^0.7.1", "react-router-dom": "^7.2.0", - "react-toastify": "^11.0.5", "serve": "^14.2.4", "styled-components": "^6.1.15", "tailwindcss": "^4.0.9" @@ -36,7 +35,6 @@ "@eslint/js": "^9.19.0", "@storybook/addon-a11y": "^8.6.12", "@storybook/addon-essentials": "^8.6.12", - "@storybook/addon-interactions": "^8.6.12", "@storybook/addon-onboarding": "^8.6.12", "@storybook/blocks": "^8.6.12", "@storybook/experimental-addon-test": "^8.6.12", @@ -2143,27 +2141,6 @@ "storybook": "^8.6.12" } }, - "node_modules/@storybook/addon-interactions": { - "version": "8.6.12", - "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-8.6.12.tgz", - "integrity": "sha512-cTAJlTq6uVZBEbtwdXkXoPQ4jHOAGKQnYSezBT4pfNkdjn/FnEeaQhMBDzf14h2wr5OgBnJa6Lmd8LD9ficz4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "@storybook/instrumenter": "8.6.12", - "@storybook/test": "8.6.12", - "polished": "^4.2.2", - "ts-dedent": "^2.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^8.6.12" - } - }, "node_modules/@storybook/addon-measure": { "version": "8.6.12", "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.6.12.tgz", @@ -2372,7 +2349,6 @@ "resolved": "https://registry.npmjs.org/@storybook/experimental-addon-test/-/experimental-addon-test-8.6.12.tgz", "integrity": "sha512-auc8Ql0buH0WeaKVuSSuabxIiBWvqvAyxtXCm1sVMkL68GwrX3cmpNMwviz3mvKvM//F8zKi/31HMl1PZ5UnIA==", "dev": true, - "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^1.2.12", @@ -7887,19 +7863,6 @@ "react-dom": ">=18" } }, - "node_modules/react-toastify": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", - "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": "^18 || ^19", - "react-dom": "^18 || ^19" - } - }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index 30c27633..f4e9a46d 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "react-dom": "^19.0.0", "react-pro-sidebar": "^0.7.1", "react-router-dom": "^7.2.0", - "react-toastify": "^11.0.5", "serve": "^14.2.4", "styled-components": "^6.1.15", "tailwindcss": "^4.0.9" @@ -40,7 +39,6 @@ "@eslint/js": "^9.19.0", "@storybook/addon-a11y": "^8.6.12", "@storybook/addon-essentials": "^8.6.12", - "@storybook/addon-interactions": "^8.6.12", "@storybook/addon-onboarding": "^8.6.12", "@storybook/blocks": "^8.6.12", "@storybook/experimental-addon-test": "^8.6.12", diff --git a/src/Dominio/Modelos/Cuotas/CuotaSetModelo.js b/src/Dominio/Modelos/Cuotas/CuotaSetModelo.js index dd8148ae..34e151a3 100644 --- a/src/Dominio/Modelos/Cuotas/CuotaSetModelo.js +++ b/src/Dominio/Modelos/Cuotas/CuotaSetModelo.js @@ -1,4 +1,6 @@ - export class CuotaSetModelo { +//RF[31] Consulta crear set de cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF31] + +export class CuotaSetModelo { constructor({ nombreCuotaSet, descripcion, diff --git a/src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js b/src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js new file mode 100644 index 00000000..95151c7d --- /dev/null +++ b/src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js @@ -0,0 +1,14 @@ +/* + * Modelo de un grupo de empleados + * RF[23] Lee grupo de empleados - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF23 + */ + +export class GrupoEmpleadosLectura { + constructor({ idGrupo, nombre, descripcion, setsProductos = [], empleados = [] }) { + this.idGrupo = idGrupo; + this.nombre = nombre; + this.descripcion = descripcion; + this.setsProductos = setsProductos; + this.empleados = empleados; + } +} diff --git a/src/Dominio/Modelos/Pagos/ListaTipoPago.js b/src/Dominio/Modelos/Pagos/ListaTipoPago.js new file mode 100644 index 00000000..f51430ae --- /dev/null +++ b/src/Dominio/Modelos/Pagos/ListaTipoPago.js @@ -0,0 +1,12 @@ +import { TipoPago } from '@Modelos/Pagos/TipoPago'; + +//RF[52] Consulta Lista de Pago - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF52] + +export class ListaTipoPago { + constructor({ mensaje, listaTipoPagos }) { + this.mensaje = mensaje; + this.listaTipoPagos = Array.isArray(listaTipoPagos) + ? listaTipoPagos.map((tipoPago) => new TipoPago(tipoPago)) + : []; + } +} diff --git a/src/Dominio/Modelos/Pagos/TipoPago.js b/src/Dominio/Modelos/Pagos/TipoPago.js new file mode 100644 index 00000000..fdc30309 --- /dev/null +++ b/src/Dominio/Modelos/Pagos/TipoPago.js @@ -0,0 +1,9 @@ +//RF[52] Consulta Lista de Pago - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF52] + +export class TipoPago { + constructor({ idTipoPago, metodo, habilitado }) { + this.idTipoPago = idTipoPago; + this.metodo = metodo; + this.habilitado = Boolean(habilitado); + } +} diff --git a/src/Dominio/Modelos/SetsProductos/SetProductos.js b/src/Dominio/Modelos/SetsProductos/SetProductos.js index dfb09f4d..b36cd24a 100644 --- a/src/Dominio/Modelos/SetsProductos/SetProductos.js +++ b/src/Dominio/Modelos/SetsProductos/SetProductos.js @@ -3,13 +3,16 @@ * Representa los datos básicos de un set de productos asociado a un cliente. * * RF42 - Super Administrador, Cliente Consulta Lista de Sets de Productos - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF42 + * RF48 - Super Administrador, Cliente Lee Categoria de Productos - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/rf48/ */ export class SetProductos { - constructor({ idSetProducto, nombre, descripcion, activo }) { + constructor({ idSetProducto, nombre, descripcion, activo, productos, grupos }) { this.idSetProducto = idSetProducto; this.nombre = nombre; this.descripcion = descripcion; this.activo = activo; + this.productos = productos; + this.grupos = grupos; } } diff --git a/src/Dominio/Repositorios/Clientes/repositorioActualizarCliente.js b/src/Dominio/Repositorios/Clientes/repositorioActualizarCliente.js new file mode 100644 index 00000000..e5acf19b --- /dev/null +++ b/src/Dominio/Repositorios/Clientes/repositorioActualizarCliente.js @@ -0,0 +1,23 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +// RF14 - Actualiza Cliente - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF14 + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioActualizarCliente { + static async actualizarClienteConImagen(formData) { + if (!formData) return; + + try { + await axios.put(RUTAS_API.CLIENTES.ACTUALIZAR_CLIENTE, formData, { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + }); + } catch { + throw new Error('Error en el repositorio'); + } + } +} diff --git a/src/Dominio/Repositorios/Empleados/RepositorioLeerGrupoEmpleados.js b/src/Dominio/Repositorios/Empleados/RepositorioLeerGrupoEmpleados.js new file mode 100644 index 00000000..ff97c4b8 --- /dev/null +++ b/src/Dominio/Repositorios/Empleados/RepositorioLeerGrupoEmpleados.js @@ -0,0 +1,42 @@ +import axios from 'axios'; +import { GrupoEmpleadosLectura } from '@Modelos/Empleados/GrupoEmpleadosLectura'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioLeerGrupoEmpleados { + /** + * Consulta los datos de un grupo de empleados específico por ID + * @param {number} idGrupo - ID del grupo de empleados a consultar + * @returns {Promise<{grupoEmpleados: GrupoEmpleadosLectura, mensaje: string}>} + * + * @see [RF[23] Leer grupo de empleados - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF23) + */ + static async obtenerPorId(idGrupo) { + try { + const respuesta = await axios.post( + RUTAS_API.EMPLEADOS.LEER_GRUPO, + { idGrupo }, + { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + } + ); + + const { grupoEmpleados, mensaje } = respuesta.data; + + const grupoEmpleadosInstancia = new GrupoEmpleadosLectura(grupoEmpleados); + + return { + grupoEmpleados: grupoEmpleadosInstancia, + mensaje, + }; + } catch (error) { + const mensaje + = error.response?.data?.mensaje || 'Error al obtener datos del grupo de empleados.'; + throw new Error(mensaje); + } + } +} diff --git a/src/Dominio/Repositorios/Pagos/RepositorioActualizarTipoPago.js b/src/Dominio/Repositorios/Pagos/RepositorioActualizarTipoPago.js new file mode 100644 index 00000000..489e20ff --- /dev/null +++ b/src/Dominio/Repositorios/Pagos/RepositorioActualizarTipoPago.js @@ -0,0 +1,27 @@ +//RF[54] Consulta Lista de Pagos - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF54] + +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioActualizarTipoPago { + static async actualizar(cambios) { + try { + const respuesta = await axios.put( + RUTAS_API.PAGOS.ACTUALIZAR_LISTA, + { 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/Repositorios/Pagos/RepositorioConsultarTipoPagos.js b/src/Dominio/Repositorios/Pagos/RepositorioConsultarTipoPagos.js new file mode 100644 index 00000000..58acc124 --- /dev/null +++ b/src/Dominio/Repositorios/Pagos/RepositorioConsultarTipoPagos.js @@ -0,0 +1,24 @@ +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; +import { ListaTipoPago } from '@Modelos/Pagos/ListaTipoPago'; + +//RF[52] Consulta Lista de Pago - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF52] + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioConsultarTipoPagos { + static async consultarPagos() { + try { + const respuesta = await axios.get(RUTAS_API.PAGOS.CONSULTAR_LISTA, { + withCredentials: true, + headers: { + 'x-api-key': API_KEY, + }, + }); + return new ListaTipoPago(respuesta.data); + } catch (error) { + const mensaje = error?.response?.data?.mensaje; + throw new Error(mensaje); + } + } +} diff --git a/src/Dominio/Repositorios/Productos/repositorioEliminarProductos.js b/src/Dominio/Repositorios/Productos/repositorioEliminarProductos.js index d7f54121..07e31768 100644 --- a/src/Dominio/Repositorios/Productos/repositorioEliminarProductos.js +++ b/src/Dominio/Repositorios/Productos/repositorioEliminarProductos.js @@ -9,12 +9,13 @@ export class RepositorioEliminarProductos { /** * Elimina una o más productos desde la API * @param {array} idsProducto - ID de los productos a eliminar + * @param {array} imagenes - Array de imágenes a eliminar * @returns {Promise<{mensaje: string}>} */ - static async eliminarProducto(idsProducto) { + static async eliminarProducto(idsProducto, imagenes = []) { try { const respuesta = await axios.delete(RUTAS_API.PRODUCTOS.ELIMINAR_PRODUCTO, { - data: { ids: idsProducto }, + data: { ids: idsProducto, imagenes }, headers: { 'x-api-key': API_KEY, }, diff --git a/src/Dominio/Servicios/obtenerPermisos.js b/src/Dominio/Servicios/obtenerPermisos.js index f6415e5a..11b037b4 100644 --- a/src/Dominio/Servicios/obtenerPermisos.js +++ b/src/Dominio/Servicios/obtenerPermisos.js @@ -7,7 +7,7 @@ const obtenerPermisos = async () => { try { const respuesta = await axios.post( `${API_URL}/api/roles/obtener-opciones`, - {}, + {}, { headers: { 'x-api-key': API_KEY, @@ -22,8 +22,7 @@ const obtenerPermisos = async () => { })); return permisosFormateados; - } catch (error) { - console.error('Error al obtener permisos:', error); + } catch { return []; } }; diff --git a/src/Rutas/RutasSistemaAdministrativo.jsx b/src/Rutas/RutasSistemaAdministrativo.jsx index db49a7a9..498981fa 100644 --- a/src/Rutas/RutasSistemaAdministrativo.jsx +++ b/src/Rutas/RutasSistemaAdministrativo.jsx @@ -8,7 +8,6 @@ import LIstaEmpleados from '@Empleados/ListaEmpleados'; import EditarCuotas from '@Cuotas/EditarCuotas'; import ListaGrupoEmpleados from '@Empleados/ListaGrupoEmpleados'; import SistemaAdministrativo from '@Paginas/SistemaAdministrativo'; -import Configuracion from '@Configuracion/ConfiguracionGeneral'; import Error404 from '@Errores/Error404'; import ListaRoles from '@Roles/ListaRoles'; import ListaUsuarios from '@Usuarios/ListaUsuarios'; @@ -18,6 +17,7 @@ import ListaEventos from '@Eventos/ListaEventos'; import RutaProtegida from '@Rutas/RutaProtegida'; import VerificarClienteSeleccionado from '@Rutas/VerificarClienteSeleccionado'; import ListaPedidos from '@Pedidos/ListaPedidos'; +import ConfiguracionGeneral from '@Configuracion/ConfiguracionGeneral'; const RutasSistemaAdministrativo = () => { return ( @@ -32,7 +32,6 @@ const RutasSistemaAdministrativo = () => { } /> - } /> {/* Rutas del tablero */} { path={RUTAS.SISTEMA_ADMINISTRATIVO.PEDIDOS.CONSULTAR_PEDIDOS} element={} /> + } + /> {/* Rutas fuera del tablero */} { - const theme = useTheme(); - const colores = tokens(theme.palette.mode); - return ( - - - {/* Información principal */} - - - INFORMACIÓN{' '} - - - ID de Cliente:{' '} - - {idCliente} - - - - Nombre Legal:{' '} - {nombreLegal} - - - Nombre visible: {nombreVisible} - - - - {/* Información adicional */} - - - INFORMACIÓN ADICIONAL{' '} - - - Usuarios asignados:{' '} - {usuariosAsignados} - - - Empleados:{' '} - {empleados} - - - - - {/* Previsualización imagen */} - - PREVISUALIZAR IMAGEN - - - Previsualización - - {imagenURL && ( - - )} - - - ); -}; - -InfoCliente.propTypes = { - idCliente: PropTypes.string.isRequired, - nombreLegal: PropTypes.string.isRequired, - nombreVisible: PropTypes.string.isRequired, - empleados: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - usuariosAsignados: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - imagenURL: PropTypes.string, -}; - -export default InfoCliente; diff --git a/src/Vistas/Componentes/Moleculas/CuotasInfo.jsx b/src/Vistas/Componentes/Moleculas/CuotasInfo.jsx new file mode 100644 index 00000000..87afe07f --- /dev/null +++ b/src/Vistas/Componentes/Moleculas/CuotasInfo.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Texto from '@Atomos/Texto'; +import { Grid, Box, useTheme } from '@mui/material'; +import { tokens } from '@SRC/theme'; + +const CuotasInfo = ({ + nombre, + periodoRenovacion, + renovacionHabilitada, + descripcion, + ultimaActualizacion, +}) => { + const theme = useTheme(); + const colores = tokens(theme.palette.mode); + + return ( + + + + + + Información de la Cuota + + + + Nombre:{' '} + + {nombre || 'No especificado'} + + + + Descripción:{' '} + + {descripcion || 'Sin descripción'} + + + + Periodo de Renovación:{' '} + + {periodoRenovacion ?? 'No especificado'} + + + + Renovación Habilitada:{' '} + + {renovacionHabilitada === 1 || renovacionHabilitada === true ? 'Sí' : 'No'} + + + + Última Actualización:{' '} + + {ultimaActualizacion ? new Date(ultimaActualizacion).toLocaleString() : 'N/A'} + + + + + + ); +}; + +CuotasInfo.propTypes = { + nombre: PropTypes.string, + periodoRenovacion: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + renovacionHabilitada: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]), + descripcion: PropTypes.string, + ultimaActualizacion: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), +}; + +export default CuotasInfo; diff --git a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfo.jsx b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfo.jsx new file mode 100644 index 00000000..d1c65ec5 --- /dev/null +++ b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfo.jsx @@ -0,0 +1,95 @@ +import { Box, Grid, useTheme } from '@mui/material'; +import Texto from '@Atomos/Texto'; +import Chip from '@Atomos/Chip'; +import { tokens } from '@SRC/theme'; +import Tabla from '@Organismos/Tabla'; + +// RF[23] Lee grupo de empleados -https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF23 + +const InfoGrupoEmpleados = ({ descripcion, setsProductos, empleados }) => { + const theme = useTheme(); + const colores = tokens(theme.palette.mode); + + // Columnas para la tabla + const columnas = [ + { field: 'nombreCompleto', headerName: 'Nombre del Empleado', flex: 0.9 }, + { field: 'correoElectronico', headerName: 'Correo Electrónico', flex: 1 }, + { field: 'areaTrabajo', headerName: 'Área de Trabajo', flex: 0.85 }, + ]; + + // Generación de filas desde el arreglo de empleados + const filas = empleados.map((empleado, index) => { + const [nombreCompleto, correoElectronico, areaTrabajo] = empleado.split(' | '); + return { + id: index + 1, + nombreCompleto, + correoElectronico, + areaTrabajo, + }; + }); + + return ( + + + {/* Descripción */} + + + Descripción: + + + {descripcion || 'No especificada'} + + + + {/* Sets de Productos y Empleados */} + + + Sets de Productos: + + + {setsProductos?.length ? ( + setsProductos.map((set, index) => ( + + )) + ) : ( + + No especificada + + )} + + + + + + Empleados: + + + {filas.length > 0 ? ( + + ) : ( + + No especificada + + )} + + + + + ); +}; + +export default InfoGrupoEmpleados; diff --git a/src/Vistas/Componentes/Moleculas/InfoCliente.jsx b/src/Vistas/Componentes/Moleculas/InfoCliente.jsx new file mode 100644 index 00000000..0f745653 --- /dev/null +++ b/src/Vistas/Componentes/Moleculas/InfoCliente.jsx @@ -0,0 +1,267 @@ +import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; +import { Grid, Box, useTheme, Button, CircularProgress } from '@mui/material'; +import Texto from '@Atomos/Texto'; +import Icono from '@Atomos/Icono'; +import CampoTexto from '@Atomos/CampoTexto'; +import { tokens } from '@SRC/theme'; + +const InfoCliente = ({ + modoEdicion = false, + idCliente, + nombreLegal, + nombreVisible, + empleados, + usuariosAsignados, + urlImagen, + onChange, + onImageChange, + imagenSubiendo = false, +}) => { + const theme = useTheme(); + const colores = tokens(theme.palette.mode); + const fileInputRef = useRef(null); + const MAX_LENGTH = 100; + + const handleFileSelect = () => { + fileInputRef.current.click(); + }; + const handleFileChange = (evento) => { + const file = evento.target.files[0]; + if (!file) return; + + // Verificar que sea un archivo JPG o JPEG + const validJpgTypes = ['image/jpeg', 'image/jpg']; + if (!validJpgTypes.includes(file.type.toLowerCase())) { + if (onImageChange) { + onImageChange({ + error: 'Solo se permiten imágenes en formato JPG o JPEG.', + }); + } + evento.target.value = ''; // Limpiar el input para permitir seleccionar el mismo archivo nuevamente + return; + } + + // Verificar el tamaño del archivo + const maxSize = 4 * 1024 * 1024; + if (file.size > maxSize) { + if (onImageChange) { + onImageChange({ + error: 'La imagen es demasiado grande. El tamaño máximo permitido es 4MB.', + }); + } + evento.target.value = ''; // Limpiar el input para permitir seleccionar el mismo archivo nuevamente + return; + } + + if (onImageChange) { + const preview = URL.createObjectURL(file); + + onImageChange({ + file, + preview, + name: file.name, + type: file.type, + size: file.size, + }); + } + }; + return ( + + + {/* Información principal */} + + + INFORMACIÓN{' '} + + + {modoEdicion ? ( + + + + + + ) : ( + <> + + ID de Cliente:{' '} + + {idCliente} + + + + Nombre Legal:{' '} + {nombreLegal} + + + Nombre visible: {nombreVisible} + + + )} + + + {/* Información adicional (siempre no editable) */} + + + INFORMACIÓN ADICIONAL{' '} + + + + Usuarios asignados:{' '} + {usuariosAsignados} + + + Empleados:{' '} + {empleados} + + + + + {/* Previsualización imagen */} + + PREVISUALIZAR IMAGEN + + + Previsualización + {imagenSubiendo && ( + + )} + + {!urlImagen && ( + + )} + + + {modoEdicion && ( + <> + + + + Solo se permiten imágenes en formato JPG/JPEG, máximo 5MB. + + + )} + + {/* {imagenError && ( + + )} */} + + ); +}; + +InfoCliente.propTypes = { + modoEdicion: PropTypes.bool, + idCliente: PropTypes.string.isRequired, + nombreLegal: PropTypes.string.isRequired, + nombreVisible: PropTypes.string.isRequired, + empleados: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + usuariosAsignados: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + urlImagen: PropTypes.string, + onChange: PropTypes.func, + onImageChange: PropTypes.func, + imagenSubiendo: PropTypes.bool, + imagenError: PropTypes.string, +}; + +export default InfoCliente; diff --git a/src/Vistas/Componentes/Moleculas/UsuarioInfo.jsx b/src/Vistas/Componentes/Moleculas/InfoUsuario.jsx similarity index 100% rename from src/Vistas/Componentes/Moleculas/UsuarioInfo.jsx rename to src/Vistas/Componentes/Moleculas/InfoUsuario.jsx diff --git a/src/Vistas/Componentes/Moleculas/SetProductosInfo.jsx b/src/Vistas/Componentes/Moleculas/SetProductosInfo.jsx new file mode 100644 index 00000000..18aa7a06 --- /dev/null +++ b/src/Vistas/Componentes/Moleculas/SetProductosInfo.jsx @@ -0,0 +1,78 @@ +// RF48 - Super Administrador, Cliente Lee Categoria de Productos - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/rf48/ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Box, Divider } from '@mui/material'; +import Texto from '@Atomos/Texto'; + +/** + * Componente para mostrar información de un set de productos + */ +const InfoSetProductos = ({ nombre, descripcion, productos, grupos }) => { + // Normaliza a arreglo si vienen como string + const productosList + = typeof productos === 'string' + ? productos.split(',').map((prod) => prod.trim()) + : productos || []; + + const gruposList + = typeof grupos === 'string' ? grupos.split(',').map((grp) => grp.trim()) : grupos || []; + + return ( + + + {nombre || ''} + + + {descripcion && ( + + {descripcion} + + )} + + + + Productos + + + {productosList.map((producto, idx) => ( + + + {producto} + + + + ))} + + + + + Grupos de empleados asignados + + {gruposList.map((grupo, idx) => ( + + + {grupo} + + + + ))} + + + ); +}; + +InfoSetProductos.propTypes = { + nombre: PropTypes.string, + descripcion: PropTypes.string, + productos: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]), + grupos: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]), +}; + +InfoSetProductos.defaultProps = { + nombre: '', + descripcion: '', + productos: [], + grupos: [], +}; + +export default InfoSetProductos; diff --git a/src/Vistas/Componentes/Moleculas/TarjetaConImagen.jsx b/src/Vistas/Componentes/Moleculas/TarjetaConImagen.jsx index 4616b5ee..737ff7b9 100644 --- a/src/Vistas/Componentes/Moleculas/TarjetaConImagen.jsx +++ b/src/Vistas/Componentes/Moleculas/TarjetaConImagen.jsx @@ -27,6 +27,8 @@ const TarjetaConImagen = ({ bordeRedondeado = '10px', alClicImagen, alClicIcono, + truncarTitulo = true, + maxLineasTitulo = 2, }) => ( - + {titulo && ( - + {titulo} )} @@ -80,6 +97,7 @@ const TarjetaConImagen = ({ clickable={iconoClickeable} tooltip={tooltipIcono} onClick={alClicIcono} + sx={{ flexShrink: 0, ml: 1 }} /> )} @@ -109,6 +127,8 @@ TarjetaConImagen.propTypes = { bordeRedondeado: PropTypes.string, alClicImagen: PropTypes.func, alClicIcono: PropTypes.func, + truncarTitulo: PropTypes.bool, + maxLineasTitulo: PropTypes.number, }; TarjetaConImagen.defaultProps = { @@ -120,6 +140,8 @@ TarjetaConImagen.defaultProps = { ajuste: 'cover', clickeableImagen: false, estiloImagen: {}, + truncarTitulo: true, + maxLineasTitulo: 2, }; export default TarjetaConImagen; diff --git a/src/Vistas/Componentes/Organismos/Clientes/AgregarClienteTarjeta.jsx b/src/Vistas/Componentes/Organismos/Clientes/AgregarClienteTarjeta.jsx new file mode 100644 index 00000000..b92b2621 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Clientes/AgregarClienteTarjeta.jsx @@ -0,0 +1,47 @@ +import { Box } from '@mui/material'; +import Icono from '@Atomos/Icono'; +import Texto from '@Atomos/Texto'; + +const estiloTarjeta = { + minWidth: { xs: '100%', sm: '250px', md: '300px' }, + maxWidth: '100%', + flexGrow: 1, +}; + +const estiloTarjetaAgregar = { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + border: '2px dashed rgba(15, 140, 241, 0.18)', + backgroundColor: 'rgba(15, 140, 241, 0.18)', + color: '#1976D2', + borderRadius: '10px', + cursor: 'pointer', + transition: 'all 0.2s ease-in-out', + '&:hover': { + backgroundColor: 'rgba(15, 139, 241, 0.38)', + transform: 'scale(1.03)', + }, +}; + +export const AgregarClienteTarjeta = () => { + const handleAgregarCliente = () => { + console.log('Agregar cliente'); + // Aquí iría la lógica para agregar un cliente + }; + + return ( + + + + Agregar cliente + + + ); +}; diff --git a/src/Vistas/Componentes/Organismos/Clientes/ClienteTarjeta.jsx b/src/Vistas/Componentes/Organismos/Clientes/ClienteTarjeta.jsx new file mode 100644 index 00000000..f31bb2b9 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Clientes/ClienteTarjeta.jsx @@ -0,0 +1,26 @@ +import TarjetaConImagen from '@Moleculas/TarjetaConImagen'; + +export const ClienteTarjeta = ({ cliente, modoEliminacion, onClienteClick, onIconoClick }) => { + const nombreCliente = cliente.nombreComercial || cliente.nombreVisible || 'Cliente sin nombre'; + + return ( + onClienteClick(cliente.idCliente, cliente.urlImagen, nombreCliente)} + alClicIcono={() => onIconoClick(cliente, modoEliminacion)} + /> + ); +}; diff --git a/src/Vistas/Componentes/Organismos/Clientes/ClientesLista.jsx b/src/Vistas/Componentes/Organismos/Clientes/ClientesLista.jsx new file mode 100644 index 00000000..59f13fff --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Clientes/ClientesLista.jsx @@ -0,0 +1,37 @@ +import { Box } from '@mui/material'; +import { ClienteTarjeta } from '@Organismos/Clientes/ClienteTarjeta'; + +const estiloTarjeta = { + minWidth: { xs: '100%', sm: '250px', md: '300px' }, + maxWidth: '100%', + flexGrow: 1, +}; + +export const ClientesLista = ({ + clientes, + modoEliminacion, + onClienteClick, + onIconoClick, + onMouseDown, + onMouseUp, + onTouchStart, + onTouchEnd, +}) => { + return clientes.map((cliente) => ( + + + + )); +}; diff --git a/src/Vistas/Componentes/Organismos/Clientes/DetalleClienteModal.jsx b/src/Vistas/Componentes/Organismos/Clientes/DetalleClienteModal.jsx new file mode 100644 index 00000000..a72a6597 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Clientes/DetalleClienteModal.jsx @@ -0,0 +1,103 @@ +import ModalCliente from '@Organismos/Clientes/ModalLeerCliente'; +import InfoCliente from '@Moleculas/InfoCliente'; +import Texto from '@Atomos/Texto'; +import PropTypes from 'prop-types'; +import { useAuth } from '@Hooks/AuthProvider'; +import { PERMISOS } from '@Constantes/permisos'; +import { Alert, Box } from '@mui/material'; // Añadir Box + +export const DetalleClienteModal = ({ + open, + cliente, + modoEdicion, + cargando, + colores, + onClose, + onToggleEdicion, + onChange, + onImageChange, + imagenSubiendo, + imagenError, +}) => { + const { usuario } = useAuth(); + + // Verificar si hay campos vacíos para deshabilitar el botón + const camposInvalidos + = modoEdicion && cliente ? !cliente.nombreLegal?.trim() || !cliente.nombreVisible?.trim() : false; + + return ( + open && ( + + {imagenError} + + ) + } + > + {cargando ? ( + Cargando cliente... + ) : cliente ? ( + + ) : ( + No se encontró información del cliente. + )} + + ) + ); +}; + +DetalleClienteModal.propTypes = { + open: PropTypes.bool.isRequired, + cliente: PropTypes.object, + modoEdicion: PropTypes.bool.isRequired, + cargando: PropTypes.bool.isRequired, + colores: PropTypes.object.isRequired, + onClose: PropTypes.func.isRequired, + onToggleEdicion: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onImageChange: PropTypes.func, + imagenSubiendo: PropTypes.bool, + imagenError: PropTypes.string, +}; + +export default DetalleClienteModal; diff --git a/src/Vistas/Componentes/Organismos/Clientes/EliminarClientesModal.jsx b/src/Vistas/Componentes/Organismos/Clientes/EliminarClientesModal.jsx new file mode 100644 index 00000000..a733b8f0 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Clientes/EliminarClientesModal.jsx @@ -0,0 +1,44 @@ +import ModalFlotante from '@Organismos/ModalFlotante'; +import Alerta from '@Moleculas/Alerta'; +import Texto from '@Atomos/Texto'; + +export const EliminarClienteModal = ({ + open, + cliente, + onConfirm, + onCancel, + eliminacionExitosa, + errorEliminacion, + onCloseAlert, +}) => { + return ( + <> + + Esta acción no se puede deshacer. + + + {errorEliminacion && ( + + )} + + {eliminacionExitosa && ( + + )} + + ); +}; diff --git a/src/Vistas/Componentes/Organismos/ModalLeerCliente.jsx b/src/Vistas/Componentes/Organismos/Clientes/ModalLeerCliente.jsx similarity index 92% rename from src/Vistas/Componentes/Organismos/ModalLeerCliente.jsx rename to src/Vistas/Componentes/Organismos/Clientes/ModalLeerCliente.jsx index 1b5448ac..50cbcebe 100644 --- a/src/Vistas/Componentes/Organismos/ModalLeerCliente.jsx +++ b/src/Vistas/Componentes/Organismos/Clientes/ModalLeerCliente.jsx @@ -15,6 +15,7 @@ const ModalLeerCliente = ({ cancelLabel = 'Cancelar', botones = null, children, + errorPanel = null, // Nuevo prop para el panel de error }) => { const theme = useTheme(); const colores = tokens(theme.palette.mode); @@ -77,6 +78,9 @@ const ModalLeerCliente = ({ {children} + {/* Panel de error arriba de los botones */} + {errorPanel} + @@ -93,6 +97,7 @@ ModalLeerCliente.propTypes = { cancelLabel: PropTypes.string, botones: PropTypes.array, children: PropTypes.node.isRequired, + errorPanel: PropTypes.node, // Nuevo prop }; export default ModalLeerCliente; diff --git a/src/Vistas/Componentes/Organismos/FormaCrearCuotaSet.jsx b/src/Vistas/Componentes/Organismos/Cuotas/FormaCrearCuotaSet.jsx similarity index 94% rename from src/Vistas/Componentes/Organismos/FormaCrearCuotaSet.jsx rename to src/Vistas/Componentes/Organismos/Cuotas/FormaCrearCuotaSet.jsx index febbce43..c8dd6226 100644 --- a/src/Vistas/Componentes/Organismos/FormaCrearCuotaSet.jsx +++ b/src/Vistas/Componentes/Organismos/Cuotas/FormaCrearCuotaSet.jsx @@ -4,6 +4,8 @@ import { useState, useEffect } from 'react'; import obtenerProductos from '@Servicios/obtenerProductos'; import ProductosModal from '@Organismos/ProductosModal'; +//RF[31] Consulta crear set de cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF31] + const columns = [ { field: 'id', headerName: 'Id', width: 100 }, { field: 'nombreProducto', headerName: 'Nombre', width: 220 }, diff --git a/src/Vistas/Componentes/Organismos/ModalCrearCuotaSet.jsx b/src/Vistas/Componentes/Organismos/Cuotas/ModalCrearCuotaSet.jsx similarity index 95% rename from src/Vistas/Componentes/Organismos/ModalCrearCuotaSet.jsx rename to src/Vistas/Componentes/Organismos/Cuotas/ModalCrearCuotaSet.jsx index 96688e32..52254ec8 100644 --- a/src/Vistas/Componentes/Organismos/ModalCrearCuotaSet.jsx +++ b/src/Vistas/Componentes/Organismos/Cuotas/ModalCrearCuotaSet.jsx @@ -4,6 +4,8 @@ import FormaCrearCuotaSet from '@Organismos/Formularios/FormaCrearCuotaSet'; import ModalFlotante from '@Organismos/ModalFlotante'; import { RUTAS } from '@Constantes/rutas'; +//RF[31] Consulta crear set de cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF31] + /** * Modal para crear un nuevo set de cuotas. * diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCuotaSet.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCuotaSet.jsx index f8529852..78982875 100644 --- a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCuotaSet.jsx +++ b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCuotaSet.jsx @@ -5,6 +5,8 @@ import obtenerProductos from '@Servicios/obtenerProductos'; import ProductosModal from '@Organismos/ProductosModal'; import { useAuth } from '@Hooks/AuthProvider'; +//RF[31] Consulta crear set de cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF31] + const columns = [ { field: 'id', headerName: 'Id', width: 100 }, { field: 'nombreProducto', headerName: 'Nombre', width: 220 }, diff --git a/src/Vistas/Componentes/Organismos/ModalFlotante.jsx b/src/Vistas/Componentes/Organismos/ModalFlotante.jsx index 30a50223..be03191a 100644 --- a/src/Vistas/Componentes/Organismos/ModalFlotante.jsx +++ b/src/Vistas/Componentes/Organismos/ModalFlotante.jsx @@ -15,6 +15,7 @@ const ModalFlotante = ({ cancelLabel = 'Cancelar', botones = null, children, + customWidth, }) => { const theme = useTheme(); const colores = tokens(theme.palette.mode); @@ -31,7 +32,7 @@ const ModalFlotante = ({ variant: 'contained', onClick: onConfirm, color: 'error', - backgroundColor: colores.altertex[4], + backgroundColor: colores.altertex[1], }, ]; @@ -60,7 +61,7 @@ const ModalFlotante = ({ borderRadius: 2, padding: 3, outline: 'none', - width: 620, + width: customWidth || 620, }} > {titulo && ( @@ -87,6 +88,7 @@ ModalFlotante.propTypes = { cancelLabel: PropTypes.string, botones: PropTypes.array, children: PropTypes.node.isRequired, + customWidth: PropTypes.number, }; export default ModalFlotante; diff --git a/src/Vistas/Componentes/Organismos/TarjetaConfiguracionPagos.jsx b/src/Vistas/Componentes/Organismos/TarjetaConfiguracionPagos.jsx new file mode 100644 index 00000000..404044f9 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/TarjetaConfiguracionPagos.jsx @@ -0,0 +1,196 @@ +//RF[54] Actualiza Lista de Pagos - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF54] +//RF[52] Consulta Lista de Pago - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF52] + +import Contenedor from '@Atomos/Contenedor'; +import Switch from '@Atomos/Switch'; +import Boton from '@Atomos/Boton'; +import { useEffect, useState } from 'react'; +import { useConsultarTipoPagos } from '@Hooks/Pagos/useConsultarTipoPagos'; +import { useActualizarTipoPago } from '@Hooks/Pagos/useActualizarTipoPago'; +import Alerta from '@Moleculas/Alerta'; +import { tokens } from '@SRC/theme'; +import { useTheme } from '@mui/material'; + +const TarjetaConfiguracionPagos = () => { + const tema = useTheme(); + const colores = tokens(tema.palette.mode); + const { tipoPagos, tipoPagosMapeado, cargando: cargandoConsulta } = useConsultarTipoPagos(); + const { actualizar, cargando: cargandoActualizacion, error, mensaje } = useActualizarTipoPago(); + + const [creditoHabilitado, setCreditoHabilitado] = useState(false); + const [debitoHabilitado, setDebitoHabilitado] = useState(false); + const [puntosHabilitado, setPuntosHabilitado] = useState(false); + + const [estadoInicial, setEstadoInicial] = useState({ + credito: false, + debito: false, + puntos: false, + }); + + const [mostrarConfirmacion, setMostrarConfirmacion] = useState(false); + const [alerta, setAlerta] = useState(null); + + // Objeto para mapear los tipos de pago internos a los nombres de la API + const tiposPagoMap = { + credito: 'Tarjeta de Crédito', + debito: 'Tarjeta de Débito', + puntos: 'Puntos', + }; + + useEffect(() => { + if (!cargandoConsulta && tipoPagos.length > 0) { + let credito = false; + let debito = false; + let puntos = false; + + tipoPagos.forEach(({ metodo, habilitado }) => { + switch (metodo) { + case 'Tarjeta de Crédito': + setCreditoHabilitado(habilitado); + credito = habilitado; + break; + case 'Tarjeta de Débito': + setDebitoHabilitado(habilitado); + debito = habilitado; + break; + case 'Puntos': + setPuntosHabilitado(habilitado); + puntos = habilitado; + break; + } + }); + + setEstadoInicial({ credito, debito, puntos }); + } + }, [tipoPagos, cargandoConsulta]); + + // Monitor changes in mensaje or error from the hook to show alerts + useEffect(() => { + if (mensaje) { + setAlerta({ tipo: 'success', mensaje }); + } else if (error) { + setAlerta({ tipo: 'error', mensaje: error }); + } + }, [mensaje, error]); + + const handleCambioSwitch = (tipo, nuevoEstado) => { + switch (tipo) { + case 'credito': + setCreditoHabilitado(nuevoEstado); + break; + case 'debito': + setDebitoHabilitado(nuevoEstado); + break; + case 'puntos': + setPuntosHabilitado(nuevoEstado); + break; + } + setMostrarConfirmacion(true); + }; + + const confirmarCambios = async () => { + // Clear any existing alert first + setAlerta(null); + + // Obtener estado actual + const estadoActual = { + credito: creditoHabilitado, + debito: debitoHabilitado, + puntos: puntosHabilitado, + }; + + // Filtrar solo los métodos que cambiaron + const cambios = Object.keys(estadoActual) + .filter((tipo) => estadoActual[tipo] !== estadoInicial[tipo]) + .map((tipo) => ({ + id: tipoPagosMapeado[tiposPagoMap[tipo]]?.id, + metodo: tiposPagoMap[tipo], + habilitado: estadoActual[tipo], + })); + + // Solo enviar si hay cambios + if (cambios.length > 0) { + await actualizar(cambios); + + // Actualizar el estado inicial con los nuevos valores + setEstadoInicial({ + credito: creditoHabilitado, + debito: debitoHabilitado, + puntos: puntosHabilitado, + }); + + // The alert will be shown via the useEffect that monitors mensaje and error + } + + setMostrarConfirmacion(false); + }; + + const cancelarCambios = () => { + setCreditoHabilitado(estadoInicial.credito); + setDebitoHabilitado(estadoInicial.debito); + setPuntosHabilitado(estadoInicial.puntos); + setMostrarConfirmacion(false); + }; + + return ( + <> + + handleCambioSwitch('credito', evento.target.checked)} + /> + handleCambioSwitch('debito', evento.target.checked)} + /> + handleCambioSwitch('puntos', evento.target.checked)} + /> + + + {mostrarConfirmacion && ( +
+ + +
+ )} + + {alerta && ( + setAlerta(null)} + /> + )} + + ); +}; + +export default TarjetaConfiguracionPagos; diff --git a/src/Vistas/Paginas/Clientes/ListaClientes.jsx b/src/Vistas/Paginas/Clientes/ListaClientes.jsx index c1b77e10..01071606 100644 --- a/src/Vistas/Paginas/Clientes/ListaClientes.jsx +++ b/src/Vistas/Paginas/Clientes/ListaClientes.jsx @@ -1,128 +1,66 @@ +// ListaClientes.jsx +// RF14 - Actualiza Cliente - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF14 + import { useNavigate } from 'react-router-dom'; import { Box, useTheme } from '@mui/material'; import { useAuth } from '@Hooks/AuthProvider'; -import Icono from '@Atomos/Icono'; -import Cargador from '@Atomos/Cargador'; import Texto from '@Atomos/Texto'; -import Alerta from '@Moleculas/Alerta'; +import Cargador from '@Atomos/Cargador'; import NavegadorAdministrador from '@Organismos/NavegadorAdministrador'; -import TarjetaConImagen from '@Moleculas/TarjetaConImagen'; -import ModalFlotante from '@Organismos/ModalFlotante'; -import ModalCliente from '@Organismos/ModalLeerCliente'; -import InfoCliente from '@Moleculas/ClienteInfo'; -import Cookies from 'js-cookie'; import { tokens } from '@SRC/theme'; import { RUTAS } from '@Utilidades/Constantes/rutas'; -import { useConsultarClientes } from '@Hooks/Clientes/useConsultarClientes'; -import { useSeleccionarCliente } from '@Hooks/Clientes/useSeleccionarCliente'; -import { useEliminarCliente } from '@Hooks/Clientes/useEliminarCliente'; -import { useClientePorId } from '@Hooks/Clientes/useLeerCliente'; -import { useState, useEffect, useRef } from 'react'; +import { useClientes } from '@Hooks/Clientes/useClientes'; +import { ClientesLista } from '@Organismos/Clientes/ClientesLista'; +import { AgregarClienteTarjeta } from '@Organismos/Clientes/AgregarClienteTarjeta'; +import { EliminarClienteModal } from '@Organismos/Clientes/EliminarClientesModal'; +import { DetalleClienteModal } from '@Organismos/Clientes/DetalleClienteModal'; +// Estilos const estiloImagenLogo = { marginRight: '1rem' }; -const estiloTarjeta = { - minWidth: { xs: '100%', sm: '250px', md: '300px' }, - maxWidth: '100%', - flexGrow: 1, -}; - const estiloTitulo = { marginTop: { xs: '2rem', sm: '4rem', md: '6rem' }, fontSize: { xs: '2rem', sm: '2.5rem', md: '3rem', lg: '3.5rem' }, textTransform: 'uppercase', }; - const estiloSubtitulo = { marginBottom: { xs: '1.5rem', sm: '2rem', md: '2.5rem' }, fontSize: { xs: '1.2rem', sm: '1.5rem', md: '1.8rem' }, fontWeight: 500, }; -const estiloTarjetaAgregar = { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - border: '2px dashed rgba(15, 140, 241, 0.18)', - backgroundColor: 'rgba(15, 140, 241, 0.18)', - color: '#1976D2', - borderRadius: '10px', - cursor: 'pointer', - transition: 'all 0.2s ease-in-out', - '&:hover': { - backgroundColor: 'rgba(15, 139, 241, 0.38)', - transform: 'scale(1.03)', - }, -}; - const ListaClientes = () => { const theme = useTheme(); const colores = tokens(theme.palette.mode); const navigate = useNavigate(); const { cerrarSesion } = useAuth(); - const { clientes: clientesOriginales, cargando, error } = useConsultarClientes(); - const [clientes, setClientes] = useState([]); - const { seleccionarCliente } = useSeleccionarCliente(); - const [idEliminar, setIdEliminar] = useState(null); - const [eliminacionExitosa, setEliminacionExitosa] = useState(false); - const { error: errorEliminacion } = useEliminarCliente( - idEliminar, - setEliminacionExitosa, - (idClienteEliminado) => { - setClientes((prev) => prev.filter((cliente) => cliente.idCliente !== idClienteEliminado)); - Cookies.remove('imagenClienteSeleccionado'); - Cookies.remove('nombreClienteSeleccionado'); - seleccionarCliente(null); - setIdEliminar(null); - } - ); - - const [modoEliminacion, setModoEliminacion] = useState(false); - const tiempoPresionado = useRef(null); - const ignorarPrimerClick = useRef(false); - const [clienteEliminar, setClienteEliminar] = useState(null); - const [modalAbierto, setModalAbierto] = useState(false); - const manejarInicioPresionado = () => { - tiempoPresionado.current = setTimeout(() => { - setModoEliminacion(true); - ignorarPrimerClick.current = true; - }, 800); - }; - - const manejarFinPresionado = () => { - if (!modoEliminacion) { - clearTimeout(tiempoPresionado.current); - } - }; - - useEffect(() => { - const manejarClickFuera = () => { - if (ignorarPrimerClick.current) { - ignorarPrimerClick.current = false; - return; - } - if (modoEliminacion) { - setModoEliminacion(false); - } - }; - document.addEventListener('click', manejarClickFuera); - return () => document.removeEventListener('click', manejarClickFuera); - }, [modoEliminacion]); - - useEffect(() => { - if (clientesOriginales) { - setClientes(clientesOriginales); - } - }, [clientesOriginales]); - - const [idClienteDetalle, setIdClienteDetalle] = useState(null); - const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); const { - cliente, - cargando: cargandoDetalle, - error: errorDetalle, - } = useClientePorId(modalDetalleAbierto ? idClienteDetalle : null); + clientes, + cargando, + error, + modoEliminacion, + clienteEliminar, + modalEliminacionAbierto, + idClienteDetalle, + modalDetalleAbierto, + clienteEditado, + modoEdicion, + eliminacionExitosa, + errorEliminacion, + handleClienteClick, + handleIconoClick, + handleInicioPresionado, + handleFinPresionado, + confirmarEliminacion, + cancelarEliminacion, + cerrarModalDetalle, + toggleModoEdicion, + handleClienteChange, + cerrarAlertaExito, + handleImagenChange, + imagenSubiendo, + imagenError, + } = useClientes(); const manejarCerrarSesion = async () => { await cerrarSesion(); @@ -154,89 +92,10 @@ const ListaClientes = () => { variant: 'contained', color: 'error', size: 'large', - onClick: () => manejarCerrarSesion(), + onClick: manejarCerrarSesion, }, ]; - const handleClickCliente = (clienteId, urlImagen, nombreComercial) => { - const idCliente = parseInt(clienteId, 10); - seleccionarCliente(idCliente); - Cookies.set('imagenClienteSeleccionado', urlImagen, { expires: 1 }); - Cookies.set('nombreClienteSeleccionado', nombreComercial, { expires: 1 }); - }; - - const abrirModalEliminar = (cliente) => { - setClienteEliminar(cliente); - setModalAbierto(true); - }; - - const confirmarEliminacion = () => { - if (!clienteEliminar) return; - setIdEliminar(clienteEliminar.idCliente); - setModalAbierto(false); - setClienteEliminar(null); - }; - - const cancelarEliminacion = () => { - setModalAbierto(false); - setClienteEliminar(null); - }; - - const renderTarjetaCliente = (cliente) => ( - - - handleClickCliente(cliente.idCliente, cliente.urlImagen, cliente.nombreComercial) - } - alClicIcono={() => { - if (modoEliminacion) { - abrirModalEliminar(cliente); - } else { - setIdClienteDetalle(cliente.idCliente); - setModalDetalleAbierto(true); - } - }} - /> - - ); - - const renderTarjetaAgregarCliente = () => ( - console.log('Agregar cliente')} - sx={{ ...estiloTarjeta, ...estiloTarjetaAgregar }} - role='button' - tabIndex={0} - > - - - Agregar cliente - - - ); - return ( <> { /> - Bienvenid⭐ + Bienvenid@ Selecciona un cliente para gestionar su sistema o crea uno nuevo @@ -284,86 +143,44 @@ const ListaClientes = () => { gap={2} width='100%' > - {clientes.map(renderTarjetaCliente)} - {renderTarjetaAgregarCliente()} + + )}
- - Esta acción no se puede deshacer. - - - {errorEliminacion && ( - - )} - - {eliminacionExitosa && ( - setEliminacionExitosa(false)} - /> - )} - - {modalDetalleAbierto && ( - setModalDetalleAbierto(false)} - onConfirm={() => setModalDetalleAbierto(false)} - titulo={cliente?.nombreVisible || 'Cargando...'} - tituloVariant='h4' - botones={[ - { - label: 'EDITAR', - variant: 'contained', - color: 'primary', - backgroundColor: colores.altertex[1], - onClick: () => console.log('Editar cliente'), - disabled: !!errorDetalle, - }, - { - label: 'SALIR', - variant: 'outlined', - color: 'primary', - outlineColor: colores.altertex[1], - onClick: () => setModalDetalleAbierto(false), - }, - ]} - > - {cargandoDetalle ? ( - Cargando cliente... - ) : cliente ? ( - - ) : ( - No se encontró información del cliente. - )} - - )} + onCancel={cancelarEliminacion} + eliminacionExitosa={eliminacionExitosa} + errorEliminacion={errorEliminacion} + onCloseAlert={cerrarAlertaExito} + /> - {errorDetalle && ( -
- -
- )} + ); }; diff --git a/src/Vistas/Paginas/Configuracion/ConfiguracionGeneral.jsx b/src/Vistas/Paginas/Configuracion/ConfiguracionGeneral.jsx index 3754c190..738e4dcb 100644 --- a/src/Vistas/Paginas/Configuracion/ConfiguracionGeneral.jsx +++ b/src/Vistas/Paginas/Configuracion/ConfiguracionGeneral.jsx @@ -1,5 +1,14 @@ +import ContenedorLista from '@Organismos/ContenedorLista'; +import TarjetaConfiguracionPagos from '@Organismos/TarjetaConfiguracionPagos'; + +//RF[52] Consulta Lista de Pago - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF52] + const ConfiguracionGeneral = () => { - return

Configuracion

; + return ( + + + + ); }; export default ConfiguracionGeneral; diff --git a/src/Vistas/Paginas/Cuotas/EditarCuotas.jsx b/src/Vistas/Paginas/Cuotas/EditarCuotas.jsx index e70d8117..97f62953 100644 --- a/src/Vistas/Paginas/Cuotas/EditarCuotas.jsx +++ b/src/Vistas/Paginas/Cuotas/EditarCuotas.jsx @@ -7,6 +7,8 @@ import CuerpoPrincipal from '@Organismos/Cuotas/CuerpoPrincipal'; import { useAuth } from '@Hooks/AuthProvider'; import { RUTAS } from '@Constantes/rutas'; +//RF[31] Consulta crear set de cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF31] + const EditarCuotas = () => { const ubicacion = useLocation(); const navegar = useNavigate(); diff --git a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx index ba2cbf32..aec11867 100644 --- a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx +++ b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx @@ -1,14 +1,15 @@ // 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] + 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/ModalCrearCuotaSet'; +import ModalCrearCuotaSet from '@Organismos/Cuotas/ModalCrearCuotaSet'; import Alerta from '@Moleculas/Alerta'; import PopUpEliminar from '@Moleculas/PopUp'; import { RUTAS } from '@Constantes/rutas'; @@ -16,6 +17,7 @@ import { RepositorioEliminarSetCuotas } from '@Dominio/Repositorios/Cuotas/repos 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(); const { usuario } = useAuth(); @@ -69,12 +71,15 @@ const ListaCuotas = () => { nombre: cuota.nombre, periodoRenovacion: cuota.periodoRenovacion, renovacionHabilitada: cuota.renovacionHabilitada === 1, + descripcion: cuota.descripcion, + ultimaActualizacion: cuota.ultimaActualizacion, })) : []; const handleAbrirModalCrear = () => setModalCrearAbierto(true); const handleCerrarModalCrear = () => setModalCrearAbierto(false); - + const [cuotaSeleccionada, setCuotaSeleccionada] = useState(null); + const [modalAbierto, setModalAbierto] = useState(false); const manejarCancelarEliminar = () => setAbrirPopUpEliminar(false); const manejarConfirmarEliminar = async () => { @@ -160,6 +165,10 @@ const ListaCuotas = () => { const ids = Array.isArray(nuevosIds) ? nuevosIds : Array.from(nuevosIds?.ids || []); setSeleccionados(ids); }} + onRowClick={(params) => { + setCuotaSeleccionada(params.row); + setModalAbierto(true); + }} />
@@ -172,6 +181,21 @@ const ListaCuotas = () => { confirmar={manejarConfirmarEliminar} dialogo='¿Estás seguro de que deseas eliminar los sets de cuotas seleccionados? Esta acción no se puede deshacer.' /> + {modalAbierto && cuotaSeleccionada && ( + setModalAbierto(false)} + titulo='Detalles del Set de Cuotas' + > + + + )} {alerta && ( { const [empleadoSeleccionado, setEmpleadoSeleccionado] = useState(null); const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); - const MENSAJE_POPUP_ELIMINAR = - '¿Estás seguro de que deseas eliminar los empleados seleccionados? Esta acción no se puede deshacer.'; + const MENSAJE_POPUP_ELIMINAR + = '¿Estás seguro de que deseas eliminar los empleados seleccionados? Esta acción no se puede deshacer.'; const manejarCancelarEliminar = () => { setAbrirPopUpEliminar(false); diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index 2008fb37..02f3de0d 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -1,6 +1,6 @@ -//RF22 - Consulta Lista de Grupo Empleados - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF22 -//RF25 Eliminar Grupo de empleados - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF25 - +// 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'; import { useConsultarGrupos } from '@Hooks/Empleados/useConsultarGrupos'; @@ -8,13 +8,16 @@ import { tokens } from '@SRC/theme'; import { Box, useTheme } from '@mui/material'; import Alerta from '@Moleculas/Alerta'; import { useEliminarGrupoEmpleados } from '@Hooks/Empleados/useEliminarGrupoEmpleados'; -import { useState, React } from 'react'; import PopUpEliminar from '@Moleculas/PopUp'; import { useAuth } from '@Hooks/AuthProvider'; import { PERMISOS } from '@Constantes/permisos'; +import InfoGrupoEmpleados from '@Moleculas/GrupoEmpleadosInfo'; +import { useGrupoEmpleadosId } from '@Hooks/Empleados/useLeerGrupoEmpleados'; +import ModalFlotante from '@Organismos/ModalFlotante'; -const ListaEmpleados = () => { +const ListaGrupoEmpleados = () => { const { grupos, cargando, error, refetch } = useConsultarGrupos(); + const { usuario } = useAuth(); const theme = useTheme(); const colores = tokens(theme.palette.mode); const MENSAJE_POPUP_ELIMINAR @@ -24,10 +27,18 @@ const ListaEmpleados = () => { const [alerta, setAlerta] = useState(null); const { eliminar } = useEliminarGrupoEmpleados(); const [abrirPopUpEliminar, setAbrirPopUpEliminar] = useState(false); + const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); + const [idGrupoSeleccionado, setIdGrupoSeleccionado] = useState(null); + + const { + grupoEmpleados, + cargando: cargandoDetalle, + error: errorDetalle, + } = useGrupoEmpleadosId(modalDetalleAbierto ? idGrupoSeleccionado : null); const manejarCancelarEliminar = () => { setAbrirPopUpEliminar(false); }; - const { usuario } = useAuth(); + const manejarConfirmarEliminar = async () => { try { await eliminar(gruposSeleccionados); @@ -100,7 +111,6 @@ const ListaEmpleados = () => { size: 'large', backgroundColor: colores.altertex[1], }, - { variant: 'outlined', label: 'Editar', @@ -131,6 +141,7 @@ const ListaEmpleados = () => { backgroundColor: colores.altertex[1], }, ]; + return ( <> { descripcion='Gestiona y organiza los grupos de empleados registrados en el sistema.' informacionBotones={botones} > - + {error &&

Error: {error}

} { : Array.from(selectionModel?.ids || []); setGruposSeleccionados(ids); }} + onRowClick={(params) => { + setIdGrupoSeleccionado(params.row.id); + setModalDetalleAbierto(true); + }} />
@@ -171,8 +186,40 @@ const ListaEmpleados = () => { confirmar={manejarConfirmarEliminar} dialogo={MENSAJE_POPUP_ELIMINAR} /> + {modalDetalleAbierto && ( + setModalDetalleAbierto(false)} + titulo={grupoEmpleados?.nombre || 'Cargando...'} + tituloVariant='h4' + customWidth={800} + botones={[ + { + label: 'Salir', + variant: 'outlined', + color: 'primary', + outlineColor: colores.primario[10], + onClick: () => setModalDetalleAbierto(false), + }, + ]} + > + {cargandoDetalle ? ( +

Cargando información del grupo de empleados...

+ ) : errorDetalle ? ( +

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

+ ) : ( + + )} +
+ )} ); }; -export default ListaEmpleados; +export default ListaGrupoEmpleados; diff --git a/src/Vistas/Paginas/Pedidos/ListaPedidos.jsx b/src/Vistas/Paginas/Pedidos/ListaPedidos.jsx index 7b6369ac..3bbe8502 100644 --- a/src/Vistas/Paginas/Pedidos/ListaPedidos.jsx +++ b/src/Vistas/Paginas/Pedidos/ListaPedidos.jsx @@ -112,7 +112,7 @@ const ListaPedidos = () => { { label: 'Eliminar', onClick: () => { - if (seleccionados.length === 0) { + if (seleccionados.length === 0 || seleccionados.size === 0) { setAlerta({ tipo: 'error', mensaje: 'Selecciona al menos un pedido para eliminar.', @@ -124,7 +124,7 @@ const ListaPedidos = () => { setAbrirPopUpEliminar(true); } }, - disabled: !usuario?.permisos?.includes(PERMISOS.ELIMINAR_GRUPO_EMPLEADOS), + disabled: !usuario?.permisos?.includes(PERMISOS.ELIMINAR_PEDIDO), size: 'large', color: 'error', backgroundColor: colores.altertex[1], @@ -163,10 +163,8 @@ const ListaPedidos = () => { columns={columnas} rows={filas} checkboxSelection - onRowSelectionModelChange={(seleccionados) => { - const ids = Array.isArray(seleccionados) - ? seleccionados - : Array.from(seleccionados?.ids || []); + onRowSelectionModelChange={(seleccion) => { + const ids = Array.isArray(seleccion) ? seleccion : Array.from(seleccion?.ids || []); setSeleccionados(ids); }} /> diff --git a/src/Vistas/Paginas/Productos/ListaProductos.jsx b/src/Vistas/Paginas/Productos/ListaProductos.jsx index da5c7dd3..87a1d7cb 100644 --- a/src/Vistas/Paginas/Productos/ListaProductos.jsx +++ b/src/Vistas/Paginas/Productos/ListaProductos.jsx @@ -31,8 +31,13 @@ const ListaProductos = () => { const manejarConfirmarEliminar = async () => { try { - await eliminar(productosSeleccionados); - await recargar(); + const urlsImagenes = productos + .filter((pro) => productosSeleccionados.includes(pro.idProducto)) + .map((pro) => pro.urlImagen); + + + await eliminar(productosSeleccionados, urlsImagenes); + recargar(); setAlerta({ tipo: 'success', mensaje: 'Productos eliminados correctamente.', diff --git a/src/Vistas/Paginas/SetsProductos/ListaSetsProductos.jsx b/src/Vistas/Paginas/SetsProductos/ListaSetsProductos.jsx index d6d04bf0..9ab256ba 100644 --- a/src/Vistas/Paginas/SetsProductos/ListaSetsProductos.jsx +++ b/src/Vistas/Paginas/SetsProductos/ListaSetsProductos.jsx @@ -1,5 +1,6 @@ // RF42 - Super Administrador, Cliente Consulta Lista de Sets de Productos - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF42 // RF45 - Eliminar set de productos - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF45 +// RF48 - Super Administrador, Cliente Lee Categoria de Productos - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/rf48/ import React, { useState } from 'react'; import Tabla from '@Organismos/Tabla'; @@ -8,6 +9,8 @@ import Alerta from '@Moleculas/Alerta'; import Chip from '@Atomos/Chip'; import { useEliminarSetProductos } from '@Hooks/SetsProductos/useEliminarSetProductos'; import PopUp from '@Moleculas/PopUp'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import InfoSetProductos from '@Moleculas/SetProductosInfo'; import { Box, useTheme } from '@mui/material'; import { useConsultarSetsProductos } from '@Hooks/SetsProductos/useConsultarSetsProductos'; import { tokens } from '@SRC/theme'; @@ -23,13 +26,16 @@ const ListaSetsProductos = () => { const [seleccionados, setSeleccionados] = useState([]); const [alerta, setAlerta] = useState(null); - const { eliminar } = useEliminarSetProductos(); - // Estado para controlar la visualización del modal eliminar const [abrirEliminar, setAbrirPopUpEliminar] = useState(false); + const [setSeleccionado, setSetSeleccionado] = useState(null); + const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); + const { eliminar } = useEliminarSetProductos(); + const { usuario } = useAuth(); + const manejarCancelarEliminar = () => { setAbrirPopUpEliminar(false); }; - const { usuario } = useAuth(); + const manejarConfirmarEliminar = async () => { try { await eliminar(seleccionados); @@ -93,6 +99,17 @@ const ListaSetsProductos = () => { nombre: setProducto.nombre, descripcion: setProducto.descripcion, activo: setProducto.activo, + datosCompletos: { + ...setProducto, + productos: + typeof setProducto.productos === 'string' + ? setProducto.productos.split(',').map((prod) => prod.trim()) + : setProducto.productos || [], + grupos: + typeof setProducto.grupos === 'string' + ? setProducto.grupos.split(',').map((grp) => grp.trim()) + : setProducto.grupos || [], + }, })); const botones = [ @@ -152,6 +169,10 @@ const ListaSetsProductos = () => { rows={rows} loading={cargando} checkboxSelection + onRowClick={(params) => { + setSetSeleccionado(params.row.datosCompletos); + setModalDetalleAbierto(true); + }} onRowSelectionModelChange={(selectionModel) => { const ids = Array.isArray(selectionModel) ? selectionModel @@ -161,6 +182,7 @@ const ListaSetsProductos = () => { />
+ {alerta && ( { onClose={() => setAlerta(null)} /> )} + + + {/* MODAL DETALLE */} + {modalDetalleAbierto && setSeleccionado && ( + { + setModalDetalleAbierto(false); + setSetSeleccionado(null); + }} + onConfirm={() => setModalDetalleAbierto(false)} + titulo={setSeleccionado.nombre || 'Detalles del Set'} + tituloVariant='h4' + botones={[ + { + label: 'EDITAR', + variant: 'contained', + color: 'error', + backgroundColor: colores.altertex[1], + }, + { + label: 'SALIR', + variant: 'outlined', + color: 'primary', + outlineColor: colores.primario[10], + onClick: () => setModalDetalleAbierto(false), + }, + ]} + > + + + )} ); }; diff --git a/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx b/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx index 0510dd32..22b470ff 100644 --- a/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx +++ b/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx @@ -15,7 +15,7 @@ import { RUTAS } from '@Constantes/rutas'; import { useMode, tokens } from '@SRC/theme'; import NavegadorAdministrador from '@Organismos/NavegadorAdministrador'; import { useUsuarioId } from '@Hooks/Usuarios/useLeerUsuario'; -import InfoUsuario from '@Moleculas/UsuarioInfo'; +import InfoUsuario from '@Moleculas/InfoUsuario'; import PopUp from '@Moleculas/PopUp'; import { useAuth } from '@Hooks/AuthProvider'; import { PERMISOS } from '@Constantes/permisos'; diff --git a/src/Vistas/stories/ClienteInfo.stories.js b/src/Vistas/stories/ClienteInfo.stories.js index a25a26b2..87561039 100644 --- a/src/Vistas/stories/ClienteInfo.stories.js +++ b/src/Vistas/stories/ClienteInfo.stories.js @@ -1,4 +1,4 @@ -import InfoCliente from '../componentes/moleculas/ClienteInfo'; +import InfoCliente from '../Componentes/Moleculas/InfoCliente'; export default { title: 'Componentes/Moléculas/InfoCliente', @@ -28,7 +28,7 @@ const baseArgs = { }; export const InformacionCliente = { - args: { - ...baseArgs, - }, - }; \ No newline at end of file + args: { + ...baseArgs, + }, +}; diff --git a/src/Vistas/stories/ModalFlotante.stories.jsx b/src/Vistas/stories/ModalFlotante.stories.jsx index 249d6793..897c71db 100644 --- a/src/Vistas/stories/ModalFlotante.stories.jsx +++ b/src/Vistas/stories/ModalFlotante.stories.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { fn } from '@storybook/test'; import ModalFlotante from '../componentes/organismos/ModalFlotante'; import Texto from '../componentes/atomos/Texto'; -import InfoUsuario from '../componentes/moleculas/UsuarioInfo'; +import InfoUsuario from '../Componentes/Moleculas/InfoUsuario'; export default { title: '@Organismos/ModalFlotante', diff --git a/src/Vistas/stories/SetProductosInfo.stories.js b/src/Vistas/stories/SetProductosInfo.stories.js new file mode 100644 index 00000000..53bae44a --- /dev/null +++ b/src/Vistas/stories/SetProductosInfo.stories.js @@ -0,0 +1,29 @@ +import SetProductosInfo from '../componentes/moleculas/SetProductosInfo'; + +export default { + title: 'Componentes/Moléculas/SetProductosInfo', + component: SetProductosInfo, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + nombre: { control: 'text' }, + descripcion: { control: 'text' }, + productos: { control: 'array' }, + grupos: { control: 'array' }, + }, +}; + +const baseArgs = { + nombre: 'Nombre del set de productos', + descripcion: 'Uniforme que permite comodidad y movilidad...', + productos: ['Botas', 'Chaleco', 'Playera'], + grupos: ['Atención al Cliente', 'Recursos humanos'], +}; + +export const InformacionSetProductos = { + args: { + ...baseArgs, + }, +}; diff --git a/src/Vistas/stories/UsuarioInfo.stories.js b/src/Vistas/stories/UsuarioInfo.stories.js index 7b3a0a3a..321811de 100644 --- a/src/Vistas/stories/UsuarioInfo.stories.js +++ b/src/Vistas/stories/UsuarioInfo.stories.js @@ -1,4 +1,4 @@ -import InfoUsuario from '../componentes/moleculas/UsuarioInfo'; +import InfoUsuario from '../Componentes/Moleculas/InfoUsuario'; export default { title: 'Componentes/Moléculas/InfoUsuario', @@ -49,11 +49,11 @@ export const Lectura = { ...baseArgs, modoEdicion: false, estadoUsuario: { - label: 'Activo', - color: 'primary', - shape: 'circular', - backgroundColor: 'rgba(24, 50, 165, 1)', - }, + label: 'Activo', + color: 'primary', + shape: 'circular', + backgroundColor: 'rgba(24, 50, 165, 1)', + }, }, }; @@ -62,10 +62,10 @@ export const Edicion = { ...baseArgs, modoEdicion: true, estadoUsuario: { - label: 'Activo', - color: 'primary', - shape: 'circular', - backgroundColor: 'rgba(24, 50, 165, 1)', - }, + label: 'Activo', + color: 'primary', + shape: 'circular', + backgroundColor: 'rgba(24, 50, 165, 1)', + }, }, -}; \ No newline at end of file +}; diff --git a/src/hooks/Clientes/useClientes.js b/src/hooks/Clientes/useClientes.js new file mode 100644 index 00000000..00948532 --- /dev/null +++ b/src/hooks/Clientes/useClientes.js @@ -0,0 +1,372 @@ +import { useState, useEffect, useRef } from 'react'; +import Cookies from 'js-cookie'; +import { useConsultarClientes } from '@Hooks/Clientes/useConsultarClientes'; +import { useSeleccionarCliente } from '@Hooks/Clientes/useSeleccionarCliente'; +import { useEliminarCliente } from '@Hooks/Clientes/useEliminarCliente'; +import { useClientePorId } from '@Hooks/Clientes/useLeerCliente'; +import { RepositorioActualizarCliente } from '@Repositorios/Clientes/repositorioActualizarCliente'; + +// RF14 - Actualiza Cliente - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF14 + +export const useClientes = () => { + // Clientes data + const { clientes: clientesOriginales, cargando, error } = useConsultarClientes(); + const [clientes, setClientes] = useState([]); + const { seleccionarCliente } = useSeleccionarCliente(); + + // Eliminación state + const [idEliminar, setIdEliminar] = useState(null); + const [eliminacionExitosa, setEliminacionExitosa] = useState(false); + const [modoEliminacion, setModoEliminacion] = useState(false); + const [clienteEliminar, setClienteEliminar] = useState(null); + const [modalEliminacionAbierto, setModalEliminacionAbierto] = useState(false); + + // Referencias para manejo de gestos + const tiempoPresionado = useRef(null); + const ignorarPrimerClick = useRef(false); + + // Modal de detalle state + const [idClienteDetalle, setIdClienteDetalle] = useState(null); + const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); + const [modoEdicion, setModoEdicion] = useState(false); + const [clienteEditado, setClienteEditado] = useState(null); + + // Estado para manejo de imágenes + const [imagenSubiendo, setImagenSubiendo] = useState(false); + const [imagenError, setImagenError] = useState(null); + const [imagenPreview, setImagenPreview] = useState(null); + const [imagenFile, setImagenFile] = useState(null); + + // Hooks para eliminar y obtener detalles + const { error: errorEliminacion } = useEliminarCliente( + idEliminar, + setEliminacionExitosa, + (idClienteEliminado) => { + setClientes((prev) => prev.filter((cliente) => cliente.idCliente !== idClienteEliminado)); + Cookies.remove('imagenClienteSeleccionado'); + Cookies.remove('nombreClienteSeleccionado'); + seleccionarCliente(null); + setIdEliminar(null); + } + ); + + const { + cliente, + cargando: cargandoDetalle, + error: errorDetalle, + } = useClientePorId(modalDetalleAbierto ? idClienteDetalle : null); + + // Cargar clientes originales cuando cambien + useEffect(() => { + if (clientesOriginales) { + setClientes(clientesOriginales); + } + }, [clientesOriginales]); + + // Actualizar clienteEditado cuando se cargue el detalle del cliente + useEffect(() => { + if (cliente) { + setClienteEditado(cliente); + setImagenPreview(cliente.urlImagen || null); + setImagenFile(null); + setImagenError(null); + } + }, [cliente]); + + useEffect(() => { + return () => { + if (imagenPreview && imagenPreview.startsWith('blob:')) { + URL.revokeObjectURL(imagenPreview); + } + }; + }, [imagenPreview]); + + // Manejar click fuera para desactivar modo eliminación + useEffect(() => { + const manejarClickFuera = () => { + if (ignorarPrimerClick.current) { + ignorarPrimerClick.current = false; + return; + } + if (modoEliminacion) { + setModoEliminacion(false); + } + }; + document.addEventListener('click', manejarClickFuera); + return () => document.removeEventListener('click', manejarClickFuera); + }, [modoEliminacion]); + + // Handlers para presionado largo + const handleInicioPresionado = () => { + tiempoPresionado.current = setTimeout(() => { + setModoEliminacion(true); + ignorarPrimerClick.current = true; + }, 800); + }; + + const handleFinPresionado = () => { + if (!modoEliminacion) { + clearTimeout(tiempoPresionado.current); + } + }; + + // Handlers para clientes + const handleClienteClick = (clienteId, urlImagen, nombreComercial) => { + const idCliente = parseInt(clienteId, 10); + seleccionarCliente(idCliente); + Cookies.set('imagenClienteSeleccionado', urlImagen, { expires: 1 }); + Cookies.set('nombreClienteSeleccionado', nombreComercial, { expires: 1 }); + }; + + const handleIconoClick = (cliente, enModoEliminacion) => { + if (enModoEliminacion) { + abrirModalEliminar(cliente); + } else { + abrirModalDetalle(cliente.idCliente); + } + }; + + // Handlers para modal de eliminación + const abrirModalEliminar = (cliente) => { + setClienteEliminar(cliente); + setModalEliminacionAbierto(true); + }; + + const confirmarEliminacion = () => { + if (!clienteEliminar) return; + setIdEliminar(clienteEliminar.idCliente); + setModalEliminacionAbierto(false); + setClienteEliminar(null); + }; + + const cancelarEliminacion = () => { + setModalEliminacionAbierto(false); + setClienteEliminar(null); + }; + + // Handlers para modal de detalle + const abrirModalDetalle = (clienteId) => { + setIdClienteDetalle(clienteId); + setModalDetalleAbierto(true); + }; + + const cerrarModalDetalle = () => { + setModoEdicion(false); + setModalDetalleAbierto(false); + // Limpiar estados de imagen al cerrar + setImagenPreview(null); + setImagenFile(null); + setImagenError(null); + }; + + const toggleModoEdicion = async () => { + if (modoEdicion) { + try { + if (!cliente) return; + + // Validar campos antes de enviar + const camposObligatorios = ['nombreLegal', 'nombreVisible']; + const MAX_LENGTH = 100; + + for (const campo of camposObligatorios) { + if (!clienteEditado[campo]) { + setImagenError(`El campo ${campo} es obligatorio`); + return; + } + + if (clienteEditado[campo].trim() === '') { + setImagenError(`El campo ${campo} no puede contener solo espacios en blanco`); + return; + } + + if (clienteEditado[campo].length > MAX_LENGTH) { + setImagenError(`El campo ${campo} no puede exceder los ${MAX_LENGTH} caracteres`); + return; + } + } + + // Validar formato y tamaño de imagen antes de enviar + if (imagenFile) { + const validJpgTypes = ['image/jpeg', 'image/jpg']; + if (!validJpgTypes.includes(imagenFile.type.toLowerCase())) { + setImagenError('Solo se permiten imágenes en formato JPG o JPEG.'); + return; + } + + const MAX_SIZE = 5 * 1024 * 1024; // 5MB en bytes + if (imagenFile.size > MAX_SIZE) { + setImagenError('La imagen no debe exceder 5MB de tamaño'); + return; + } + } + + const cambios = {}; + let tieneOtrosCambios = false; + + Object.keys(clienteEditado).forEach((key) => { + if (key === 'urlImagen' || key === 'createdAt' || key === 'updatedAt') { + return; + } + if (clienteEditado[key] !== cliente[key]) { + cambios[key] = clienteEditado[key]; + tieneOtrosCambios = true; + } + }); + + if (clienteEditado.nombreVisible !== cliente.nombreVisible) { + cambios.nombreComercial = clienteEditado.nombreVisible; + } + + if (tieneOtrosCambios || imagenFile) { + setImagenSubiendo(true); + setImagenError(null); + + const formData = new FormData(); + formData.append('idCliente', clienteEditado.idCliente); + + // Agregar campos modificados al FormData + Object.entries(cambios).forEach(([key, value]) => { + formData.append(key, value); + }); + + // Agregar imagen al FormData + if (imagenFile) { + formData.append('imagen', imagenFile); // clave debe coincidir con el backend + } + + await RepositorioActualizarCliente.actualizarClienteConImagen(formData); + + setClientes((prevClientes) => + prevClientes.map((cliente) => { + if (cliente.idCliente === clienteEditado.idCliente) { + return { + ...cliente, + ...cambios, + ...(imagenFile ? { urlImagen: imagenPreview } : {}), + }; + } + return cliente; + })); + } + + setModoEdicion(false); + } catch { + setImagenError('Error al guardar los cambios. Intente nuevamente.'); + } finally { + setImagenSubiendo(false); + } + } else { + setModoEdicion(true); + } + }; + + const handleClienteChange = (event) => { + const { name, value } = event.target; + const MAX_LENGTH = 100; + + // Validar longitud máxima de texto sin prohibir texto vacío + if (value.length > MAX_LENGTH) { + setImagenError(`El campo ${name} no puede exceder los ${MAX_LENGTH} caracteres`); + return; + } + + // Actualizar el valor del campo sin importar si está vacío + setClienteEditado((prev) => ({ + ...prev, + [name]: value, + })); + + // Limpiar error si existe y se ha corregido + if (imagenError) { + // Si el error era sobre caracteres y ahora cumplimos el requisito + if (imagenError.includes('caracteres') && value.length <= MAX_LENGTH) { + setImagenError(null); + } else if (imagenError.includes('espacios en blanco') && value.trim() !== '') { + setImagenError(null); + } else if (imagenError.includes(name)) { + setImagenError(null); + } + } + }; + + const handleImagenChange = (imageData) => { + if (imageData.error) { + setImagenError(imageData.error); + return; + } + + // Si no hay archivo, solo limpiamos error + if (!imageData.file) { + setImagenError(null); + return; + } + + // Validar que sea un archivo JPG o JPEG + const validJpgTypes = ['image/jpeg', 'image/jpg']; + if (!validJpgTypes.includes(imageData.file.type.toLowerCase())) { + setImagenError('Solo se permiten imágenes en formato JPG o JPEG.'); + return; + } + + // Validar tamaño de la imagen (5MB) + const MAX_SIZE = 5 * 1024 * 1024; + if (imageData.file.size > MAX_SIZE) { + setImagenError('La imagen no debe exceder 5MB de tamaño'); + return; + } + + // Si llegamos hasta aquí, eliminar cualquier error previo + setImagenError(null); + + setImagenFile(imageData.file); + const preview = imageData.preview || URL.createObjectURL(imageData.file); + setImagenPreview(preview); + + setClienteEditado((prev) => ({ + ...prev, + urlImagen: preview, + })); + }; + + const cerrarAlertaExito = () => { + setEliminacionExitosa(false); + }; + + return { + // Estado + clientes, + cargando, + error, + modoEliminacion, + clienteEliminar, + modalEliminacionAbierto, + idClienteDetalle, + modalDetalleAbierto, + clienteEditado, + modoEdicion, + cargandoDetalle, + errorDetalle, + eliminacionExitosa, + errorEliminacion, + + // Estados de imagen + imagenSubiendo, + imagenError, + imagenPreview, + + // Handlers + handleClienteClick, + handleIconoClick, + handleInicioPresionado, + handleFinPresionado, + confirmarEliminacion, + cancelarEliminacion, + cerrarModalDetalle, + toggleModoEdicion, + handleClienteChange, + cerrarAlertaExito, + + // Handlers de imagen + handleImagenChange, + }; +}; diff --git a/src/hooks/Clientes/useLeerCliente.js b/src/hooks/Clientes/useLeerCliente.js index 884c628f..42458697 100644 --- a/src/hooks/Clientes/useLeerCliente.js +++ b/src/hooks/Clientes/useLeerCliente.js @@ -26,10 +26,19 @@ export const useClientePorId = (idCliente) => { try { const { cliente, mensaje } = await RepositorioClientes.obtenerPorId(idCliente); - setCliente(cliente); + + const clienteNormalizado = cliente + ? { + ...cliente, + urlImagen: cliente.urlImagen || cliente.imagenURL || cliente.imagenCliente || null, + } + : null; + + setCliente(clienteNormalizado); setMensaje(mensaje); } catch (err) { setError(err.message); + setCliente(null); } finally { setCargando(false); } @@ -37,6 +46,11 @@ export const useClientePorId = (idCliente) => { if (idCliente) { obtenerCliente(); + } else { + setCliente(null); + setMensaje(''); + setCargando(false); + setError(null); } }, [idCliente]); diff --git a/src/hooks/Cuotas/useCrearCuotaSet.js b/src/hooks/Cuotas/useCrearCuotaSet.js index a13e4b60..2914f979 100644 --- a/src/hooks/Cuotas/useCrearCuotaSet.js +++ b/src/hooks/Cuotas/useCrearCuotaSet.js @@ -4,7 +4,8 @@ import axios from 'axios'; import CuotaSetModelo from '@Modelos/Cuotas/CuotaSetModelo'; import { RUTAS_API } from '@Constantes/rutasAPI'; -const API_URL = import.meta.env.VITE_API_URL; +//RF[31] Consulta crear set de cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF31] + const API_KEY = import.meta.env.VITE_API_KEY; export const useCrearCuotaSet = ({ diff --git a/src/hooks/Empleados/useLeerGrupoEmpleados.js b/src/hooks/Empleados/useLeerGrupoEmpleados.js new file mode 100644 index 00000000..984e0e23 --- /dev/null +++ b/src/hooks/Empleados/useLeerGrupoEmpleados.js @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import { RepositorioLeerGrupoEmpleados } from '@Repositorios/Empleados/RepositorioLeerGrupoEmpleados'; + +/** + * * Hook para obtener los datos de un grupo de empleados por su ID + * @param {number} idGrupo - ID del grupo de empleados a consultar + * @returns {{ + * grupoEmpleados: GrupoEmpleadosLectura | null, + * mensaje: string, + * cargando: boolean, + * error: string | null + * }} + * @see [RF[23] Leer grupo de empleados - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF23) + */ + +export const useGrupoEmpleadosId = (idGrupo) => { + const [grupoEmpleados, setGrupoEmpleados] = useState(null); + const [mensaje, setMensaje] = useState(''); + const [cargando, setCargando] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const obtenerGrupoEmpleados = async () => { + setCargando(true); + setError(null); + + try { + const { grupoEmpleados, mensaje } = await RepositorioLeerGrupoEmpleados.obtenerPorId( + idGrupo + ); + + setGrupoEmpleados(grupoEmpleados); + setMensaje(mensaje); + } catch (err) { + setError(err.message); + } finally { + setCargando(false); + } + }; + + if (idGrupo) { + obtenerGrupoEmpleados(); + } + }, [idGrupo]); + + return { grupoEmpleados, mensaje, cargando, error }; +}; diff --git a/src/hooks/Pagos/useActualizarTipoPago.js b/src/hooks/Pagos/useActualizarTipoPago.js new file mode 100644 index 00000000..b0275de1 --- /dev/null +++ b/src/hooks/Pagos/useActualizarTipoPago.js @@ -0,0 +1,27 @@ +//RF[54] Consulta Lista de Pagos - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF54] + +import { useState } from 'react'; +import { RepositorioActualizarTipoPago } from '@Repositorios/Pagos/RepositorioActualizarTipoPago'; + +export function useActualizarTipoPago() { + 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 RepositorioActualizarTipoPago.actualizar(cambios); + setMensaje(respuesta.mensaje || 'Actualización exitosa'); + } catch (err) { + setError(err.message || 'Ocurrió un error'); + } finally { + setCargando(false); + } + }; + + return { actualizar, cargando, error, mensaje }; +} diff --git a/src/hooks/Pagos/useConsultarTipoPagos.js b/src/hooks/Pagos/useConsultarTipoPagos.js new file mode 100644 index 00000000..4ab5a0d3 --- /dev/null +++ b/src/hooks/Pagos/useConsultarTipoPagos.js @@ -0,0 +1,63 @@ +//RF[52] Consulta Lista de Pago - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF52] + +import { useState, useEffect, useMemo } from 'react'; +import { useAuth } from '@Hooks/AuthProvider'; +import { RepositorioConsultarTipoPagos } from '@Repositorios/Pagos/RepositorioConsultarTipoPagos'; +import { PERMISOS } from '@Constantes/permisos'; + +export function useConsultarTipoPagos() { + const [tipoPagos, setTipoPagos] = useState([]); + const [mensaje, setMensaje] = useState(''); + const [cargando, setCargando] = useState(true); + const [error, setError] = useState(null); + const { usuario } = useAuth(); + const [recargarToken, setRecargarToken] = useState(1); + + useEffect(() => { + const cargar = async () => { + setCargando(true); + setError(null); + + if (!usuario?.permisos?.includes(PERMISOS.CONSULTAR_TIPOS_PAGO)) { + setCargando(false); + return; + } + + try { + const resultado = await RepositorioConsultarTipoPagos.consultarPagos(); + setTipoPagos(resultado.listaTipoPagos); + setMensaje(resultado.mensaje || 'Consulta exitosa'); + } catch (err) { + setTipoPagos([]); + setMensaje(''); + setError(err.message); + } finally { + setCargando(false); + } + }; + + cargar(); + }, [recargarToken, usuario?.permisos]); + + const recargar = () => setRecargarToken((prev) => prev + 1); + + // ✅ Objeto con los métodos como claves e id/habilitado como valores + const tipoPagosMapeado = useMemo(() => { + return tipoPagos.reduce((acc, tipo) => { + acc[tipo.metodo] = { + id: tipo.idTipoPago, + habilitado: tipo.habilitado, + }; + return acc; + }, {}); + }, [tipoPagos]); + + return { + tipoPagos, // arreglo original con idTipoPago, metodo, habilitado + tipoPagosMapeado, // objeto: { credito: { id, habilitado }, ... } + mensaje, + cargando, + error, + recargar, + }; +} diff --git a/src/hooks/Productos/useEliminarProductos.js b/src/hooks/Productos/useEliminarProductos.js index 23c6931b..534cd218 100644 --- a/src/hooks/Productos/useEliminarProductos.js +++ b/src/hooks/Productos/useEliminarProductos.js @@ -17,12 +17,12 @@ export function useEliminarProductos() { const [cargando, setCargando] = useState(false); const [error, setError] = useState(null); - const eliminar = async (idsProductos) => { + const eliminar = async (idsProductos, urlsImagenes) => { setCargando(true); setError(null); try { - const { mensaje } = await RepositorioEliminarProductos.eliminarProducto(idsProductos); + const { mensaje } = await RepositorioEliminarProductos.eliminarProducto(idsProductos, urlsImagenes); setMensaje(mensaje); } catch (err) { setMensaje(''); diff --git a/src/hooks/Roles/useEliminarRol.js b/src/hooks/Roles/useEliminarRol.js index 018edd58..96b6a7ed 100644 --- a/src/hooks/Roles/useEliminarRol.js +++ b/src/hooks/Roles/useEliminarRol.js @@ -24,7 +24,6 @@ export function useEliminarRol() { try { const { mensaje } = await RepositorioEliminarRol.eliminarRol(idsRol); - console.log(mensaje); setMensaje(mensaje); } catch (err) { setMensaje(''); diff --git a/src/theme.js b/src/theme.js index 6ccf4fb4..e86ffa87 100644 --- a/src/theme.js +++ b/src/theme.js @@ -50,6 +50,7 @@ export const tokens = (mode) => ({ 1: 'rgba(255, 255, 255, 0.08)', 2: 'rgba(255, 255, 255, 0.10)', 3: 'rgba(0, 0, 0, 0.5)', + 4: 'rgba(136, 136, 136, 0.62)', }, } : { @@ -97,6 +98,7 @@ export const tokens = (mode) => ({ 1: 'rgba(0, 0, 0, 0.08)', 2: 'rgba(0, 0, 0, 0.10)', 3: 'rgba(0, 0, 0, 0.5)', + 4: 'rgba(0, 0, 0, 0.4)', }, }), }); From be615d3e7e825ea65930a8ca7d85c6bf4d1e378a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Antonio=20Ben=C3=ADtez=20De=20La=20Portilla?= Date: Mon, 12 May 2025 21:21:00 -0600 Subject: [PATCH 005/160] =?UTF-8?q?feat:=20update=20correg=C3=AD=20el=20er?= =?UTF-8?q?ror=20que=20me=20daba=20cargando...=20y=20ya=20funciona,=20pues?= =?UTF-8?q?=20al=20modal=20de=20ListaCuotas=20se=20le=20cmabio=20las=20pro?= =?UTF-8?q?ps=20de=20Cuotasinfo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Cuotas/repositorioLeerCuota.js | 13 ++++------ src/Vistas/Paginas/Cuotas/ListaCuotas.jsx | 24 +++++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/Dominio/Repositorios/Cuotas/repositorioLeerCuota.js b/src/Dominio/Repositorios/Cuotas/repositorioLeerCuota.js index 4027d79e..dd5e9121 100644 --- a/src/Dominio/Repositorios/Cuotas/repositorioLeerCuota.js +++ b/src/Dominio/Repositorios/Cuotas/repositorioLeerCuota.js @@ -12,11 +12,11 @@ export class RepositorioLeerCuota { * * @see [RF[38] Leer cuota - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF38)] */ - static async obtenerPorId(idCuota) { + static async obtenerPorId(idSetCuota) { try { const respuesta = await axios.post( - RUTAS_API.CUOTAS.CONSULTAR_CUOTA, - { idCuota }, + RUTAS_API.CUOTAS.LEER_SET_CUOTAS, + { idSetCuota }, { headers: { 'x-api-key': API_KEY, @@ -24,13 +24,10 @@ export class RepositorioLeerCuota { withCredentials: true, } ); - - const { cuota, mensaje } = respuesta.data; - - const cuotaInstancia = new LeerCuota(cuota); + const { setCuota, mensaje } = respuesta.data; return { - cuota: cuotaInstancia, + cuota: setCuota, mensaje, }; } catch (error) { diff --git a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx index aec11867..fde4e5ce 100644 --- a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx +++ b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx @@ -1,5 +1,6 @@ // 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'; @@ -11,6 +12,7 @@ import Tabla from '@Organismos/Tabla'; import Chip from '@Atomos/Chip'; import ModalCrearCuotaSet from '@Organismos/Cuotas/ModalCrearCuotaSet'; 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'; @@ -18,10 +20,12 @@ 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(); const { usuario } = useAuth(); const { cuotas, cargando, error } = useConsultarCuotas(); + const [idCuotaDetalle, setIdCuotaDetalle] = useState(null); const theme = useTheme(); const colores = tokens(theme.palette.mode); @@ -30,6 +34,9 @@ const ListaCuotas = () => { const [idsSetCuotas, setIdsSetCuotas] = useState([]); const [alerta, setAlerta] = useState(null); const [abrirPopUpEliminar, setAbrirPopUpEliminar] = useState(false); + const [modalAbierto, setModalAbierto] = useState(false); + + const { cuota, cargando: cargandoDetalle } = useCuotaId(idCuotaDetalle); useEffect(() => { if (!usuario?.clienteSeleccionado) { @@ -68,6 +75,7 @@ const ListaCuotas = () => { const filas = Array.isArray(cuotas) ? cuotas.map((cuota) => ({ id: cuota.idCuotaSet, + idCuotaSet: cuota.idCuotaSet, nombre: cuota.nombre, periodoRenovacion: cuota.periodoRenovacion, renovacionHabilitada: cuota.renovacionHabilitada === 1, @@ -78,8 +86,6 @@ const ListaCuotas = () => { const handleAbrirModalCrear = () => setModalCrearAbierto(true); const handleCerrarModalCrear = () => setModalCrearAbierto(false); - const [cuotaSeleccionada, setCuotaSeleccionada] = useState(null); - const [modalAbierto, setModalAbierto] = useState(false); const manejarCancelarEliminar = () => setAbrirPopUpEliminar(false); const manejarConfirmarEliminar = async () => { @@ -136,7 +142,6 @@ const ListaCuotas = () => { backgroundColor: colores.altertex[1], }, ]; - return ( <> { setSeleccionados(ids); }} onRowClick={(params) => { - setCuotaSeleccionada(params.row); + setIdCuotaDetalle(params.row.idCuotaSet); setModalAbierto(true); }} /> @@ -181,19 +186,14 @@ const ListaCuotas = () => { confirmar={manejarConfirmarEliminar} dialogo='¿Estás seguro de que deseas eliminar los sets de cuotas seleccionados? Esta acción no se puede deshacer.' /> - {modalAbierto && cuotaSeleccionada && ( + + {modalAbierto && ( setModalAbierto(false)} titulo='Detalles del Set de Cuotas' > - + {cargandoDetalle || !cuota ? Cargando... : } )} From a3352016c858b3b8ffe1bca3abdbfd0efb1d6a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Antonio=20Ben=C3=ADtez=20De=20La=20Portilla?= Date: Mon, 12 May 2025 22:14:15 -0600 Subject: [PATCH 006/160] =?UTF-8?q?fix:elimine=20un=20bot=C3=B3n=20del=20m?= =?UTF-8?q?odal=20flotante?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Vistas/Paginas/Cuotas/ListaCuotas.jsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx index fde4e5ce..3ccd57d1 100644 --- a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx +++ b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx @@ -193,6 +193,15 @@ const ListaCuotas = () => { onClose={() => setModalAbierto(false)} titulo='Detalles del Set de Cuotas' > + botones ={' '} + {[ + { + label: 'Cerrar', + variant: 'contained', + onClick: () => setModalAbierto(false), + color: 'error', + }, + ]} {cargandoDetalle || !cuota ? Cargando... : } )} From f6f685705368c107baadceb99cac423483834b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Antonio=20Ben=C3=ADtez=20De=20La=20Portilla?= Date: Mon, 12 May 2025 22:16:27 -0600 Subject: [PATCH 007/160] =?UTF-8?q?fix:=20se=20removi=C3=B3=20boton=20sin?= =?UTF-8?q?=20uso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Vistas/Paginas/Cuotas/ListaCuotas.jsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx index 3ccd57d1..fde4e5ce 100644 --- a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx +++ b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx @@ -193,15 +193,6 @@ const ListaCuotas = () => { onClose={() => setModalAbierto(false)} titulo='Detalles del Set de Cuotas' > - botones ={' '} - {[ - { - label: 'Cerrar', - variant: 'contained', - onClick: () => setModalAbierto(false), - color: 'error', - }, - ]} {cargandoDetalle || !cuota ? Cargando... : } )} From 4ca508420269c7cecad0214b65dd806a13f23dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Antonio=20Ben=C3=ADtez=20De=20La=20Portilla?= Date: Mon, 12 May 2025 22:17:15 -0600 Subject: [PATCH 008/160] =?UTF-8?q?fix:=20ajustar=20formato=20de=20declara?= =?UTF-8?q?ci=C3=B3n=20de=20constante=20MENSAJE=5FPOPUP=5FELIMINAR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index 43e8cc16..24cb93a3 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -20,8 +20,8 @@ const ListaGrupoEmpleados = () => { const { usuario } = useAuth(); const theme = useTheme(); const colores = tokens(theme.palette.mode); - const MENSAJE_POPUP_ELIMINAR = - '¿Estás seguro de que deseas eliminar los grupos seleccionados? Esta acción no se puede deshacer.'; + const MENSAJE_POPUP_ELIMINAR + = '¿Estás seguro de que deseas eliminar los grupos seleccionados? Esta acción no se puede deshacer.'; const [gruposSeleccionados, setGruposSeleccionados] = useState([]); const [alerta, setAlerta] = useState(null); From 2644e13ec6d156a4258d5a8c25b9d95c7d067395 Mon Sep 17 00:00:00 2001 From: angieriosc Date: Tue, 13 May 2025 02:44:21 -0600 Subject: [PATCH 009/160] =?UTF-8?q?Feat:=20L=C3=B3gica=20implementada=20pa?= =?UTF-8?q?ra=20actualizar=20grupo=20de=20empleados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RepositorioActualizarGrupoEmpleados.js | 45 ++++ src/Dominio/Servicios/obtenerEmpleados.js | 28 +++ src/Dominio/Servicios/obtenerSetsProductos.js | 31 +++ src/Utilidades/Constantes/rutasAPI.js | 3 + .../Moleculas/GrupoEmpleadosInfoEditable.jsx | 210 ++++++++++++++++++ .../Paginas/Empleados/ListaGrupoEmpleados.jsx | 61 ++++- .../Empleados/useActualizarGrupoEmpleados.js | 58 +++++ 7 files changed, 428 insertions(+), 8 deletions(-) create mode 100644 src/Dominio/Repositorios/Empleados/RepositorioActualizarGrupoEmpleados.js create mode 100644 src/Dominio/Servicios/obtenerEmpleados.js create mode 100644 src/Dominio/Servicios/obtenerSetsProductos.js create mode 100644 src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx create mode 100644 src/hooks/Empleados/useActualizarGrupoEmpleados.js diff --git a/src/Dominio/Repositorios/Empleados/RepositorioActualizarGrupoEmpleados.js b/src/Dominio/Repositorios/Empleados/RepositorioActualizarGrupoEmpleados.js new file mode 100644 index 00000000..72de1e5b --- /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: nombre, + descripcion: descripcion, + empleados: empleados, + setsDeProductos: setsDeProductos, + }, + { + headers: { + 'x-api-key': API_KEY, + }, + withCredentials: true, + } + ); + + const { mensaje } = respuesta.data; + + return { mensaje }; + } catch (error) { + const mensaje = error.response?.data?.mensaje || 'Error al actualizar el grupo de empleados.'; + throw new Error(mensaje); + } + } +} diff --git a/src/Dominio/Servicios/obtenerEmpleados.js b/src/Dominio/Servicios/obtenerEmpleados.js new file mode 100644 index 00000000..babaca66 --- /dev/null +++ b/src/Dominio/Servicios/obtenerEmpleados.js @@ -0,0 +1,28 @@ +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, + nombre: empleado.nombreCompleto, + area: empleado.areaTrabajo, + })); + return filasFormateadas; + } catch { + return []; + } +}; +export default obtenerEmpleados; 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/rutasAPI.js b/src/Utilidades/Constantes/rutasAPI.js index 2c49378c..7bcba54c 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`; @@ -49,6 +51,7 @@ export const RUTAS_API = { ELIMINAR_EMPLEADO: `${BASE_EMPLEADOS}/eliminar`, ELIMINAR_GRUPO: `${BASE_EMPLEADOS}/eliminar-grupo`, LEER_GRUPO: `${BASE_EMPLEADOS}/leer-grupo`, + ACTUALIZAR_GRUPO: `${BASE_EMPLEADOS}/actualizar-grupo`, }, CUOTAS: { BASE: BASE_CUOTAS, diff --git a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx new file mode 100644 index 00000000..2d5cb4bc --- /dev/null +++ b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx @@ -0,0 +1,210 @@ +import Alerta from '@Moleculas/Alerta'; +import CampoTexto from '@Atomos/CampoTexto'; +import { useState, useEffect } from 'react'; +import obtenerSetsProductos from '@Servicios/obtenerSetsProductos'; +import obtenerEmpleados from '@Servicios/obtenerEmpleados'; +import ProductosModal from '@Organismos/ProductosModal'; +import { useAuth } from '@Hooks/AuthProvider'; +import Tabla from '@Organismos/Tabla'; +import { Box, Button, Chip, Grid } from '@mui/material'; +import Texto from '@Atomos/Texto'; + +const InfoGrupoEmpleadosEditable = ({ + nombre: nombreInicial, + descripcion: descripcionInicial, + setsProductos: setsProductosInicial, + empleados: empleadosInicial, +}) => { + const [productosDisponibles, setProductosDisponibles] = useState([]); + const [empleadosDisponibles, setEmpleadosDisponibles] = useState([]); // Lista de empleados disponibles + const { usuario } = useAuth(); + const clienteSeleccionado = usuario.clienteSeleccionado; + + // Estados locales + const [nombre, setNombre] = useState(nombreInicial || ''); + const [descripcion, setDescripcion] = useState(descripcionInicial || ''); + const [setsProductos, setSetsProductos] = useState(setsProductosInicial || []); + const [empleados, setEmpleados] = useState(empleadosInicial || []); + const [mostrarAlerta, setMostrarAlerta] = useState(false); + + useEffect(() => { + const obtenerDatos = async () => { + const productos = await obtenerSetsProductos(clienteSeleccionado); + setProductosDisponibles(productos); + + const empleados = await obtenerEmpleados(clienteSeleccionado); // Obtener empleados disponibles + setEmpleadosDisponibles(empleados); + }; + + obtenerDatos(); + }, [clienteSeleccionado]); + + const handleAgregarProducto = (evento) => { + const productoSeleccionado = evento.row; + + const yaExiste = setsProductos.some((producto) => producto.id === productoSeleccionado.id); + if (!yaExiste) { + setSetsProductos((prev) => [...prev, productoSeleccionado]); + } + }; + + const handleAgregarEmpleado = (evento) => { + const empleadoSeleccionado = evento.row; + + const yaExiste = empleados.some((empleado) => empleado.id === empleadoSeleccionado.id); + if (!yaExiste) { + setEmpleados((prev) => [...prev, empleadoSeleccionado]); + } + }; + + const filasEmpleados = empleados.map((empleado, index) => { + const [nombreCompleto, correoElectronico, areaTrabajo] = empleado.split(' | '); + return { + id: index + 1, + nombreCompleto, + correoElectronico, + areaTrabajo, + }; + }); + + const handleGuardar = () => { + if (!nombre || !descripcion || setsProductos.length === 0 || empleados.length === 0) { + setMostrarAlerta(true); // Mostrar alerta si faltan datos + return; + } + + console.log('Nombre:', nombre); + console.log('Descripción:', descripcion); + console.log('Sets de Productos:', setsProductos); + console.log('Empleados:', empleados); + // Aquí puedes implementar la lógica para guardar los cambios + }; + + return ( + + + {/* Nombre */} + + Nombre: + setNombre(e.target.value)} + sx={{ mt: 1 }} + /> + + + {/* Descripción */} + + Descripción: + setDescripcion(e.target.value)} + sx={{ mt: 1 }} + /> + + + {/* Sets de Productos */} + + Sets de Productos: + + {setsProductos?.length > 0 ? ( + setsProductos.map((set, index) => ( + setSetsProductos(setsProductos.filter((_, i) => i !== index))} + sx={{ + borderRadius: '16px', + backgroundColor: '#e0f7fa', + color: '#006064', + }} + /> + )) + ) : ( + + No especificada + + )} + + + + + {/* Empleados */} + + Empleados: + + {empleados?.length > 0 ? ( + empleados.map((empleado, index) => ( + setEmpleados(empleados.filter((_, i) => i !== index))} + sx={{ + borderRadius: '16px', + backgroundColor: '#e0f7fa', + color: '#006064', + }} + /> + )) + ) : ( + + No especificada + + )} + + + + + {/* Botón Guardar */} + + + + + + {mostrarAlerta && ( + setMostrarAlerta(false)} + sx={{ mb: 2, mt: 2 }} + /> + )} + + ); +}; + +export default InfoGrupoEmpleadosEditable; diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index 43e8cc16..0d25b163 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -14,6 +14,7 @@ import { PERMISOS } from '@Constantes/permisos'; import InfoGrupoEmpleados from '@Moleculas/GrupoEmpleadosInfo'; import { useGrupoEmpleadosId } from '@Hooks/Empleados/useLeerGrupoEmpleados'; import ModalFlotante from '@Organismos/ModalFlotante'; +import InfoGrupoEmpleadosEditable from '@Moleculas/GrupoEmpleadosInfoEditable'; const ListaGrupoEmpleados = () => { const { grupos, cargando, error, refetch } = useConsultarGrupos(); @@ -29,6 +30,7 @@ const ListaGrupoEmpleados = () => { const [abrirPopUpEliminar, setAbrirPopUpEliminar] = useState(false); const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); const [idGrupoSeleccionado, setIdGrupoSeleccionado] = useState(null); + const [abrirModalEditar, setAbrirModalEditar] = useState(false); const { grupoEmpleados, @@ -98,14 +100,6 @@ const ListaGrupoEmpleados = () => { size: 'large', backgroundColor: colores.altertex[1], }, - { - variant: 'outlined', - label: 'Editar', - onClick: () => console.log('Editar'), - color: 'primary', - size: 'large', - outlineColor: colores.primario[10], - }, { label: 'Eliminar', onClick: () => { @@ -180,6 +174,16 @@ const ListaGrupoEmpleados = () => { tituloVariant='h4' customWidth={800} botones={[ + { + label: 'Editar', + variant: 'contained', + color: 'primary', + outlineColor: colores.primario[10], + onClick: () => { + setModalDetalleAbierto(false); + setAbrirModalEditar(true); + }, + }, { label: 'Salir', variant: 'outlined', @@ -204,6 +208,47 @@ const ListaGrupoEmpleados = () => { )} )} + {abrirModalEditar && ( + setAbrirModalEditar(false)} + titulo='Editar Grupo de Empleados' + tituloVariant='h4' + customWidth={700} + botones={[ + { + label: 'Guardar', + variant: 'contained', + color: 'primary', + outlineColor: colores.primario[10], + onClick: () => { + setAbrirModalEditar(false); + refetch(); + }, + }, + { + 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/hooks/Empleados/useActualizarGrupoEmpleados.js b/src/hooks/Empleados/useActualizarGrupoEmpleados.js new file mode 100644 index 00000000..f67b327e --- /dev/null +++ b/src/hooks/Empleados/useActualizarGrupoEmpleados.js @@ -0,0 +1,58 @@ +import { useEffect, useState } from 'react'; +import { RepositorioActualizarGrupoEmpleados } from '@Repositorios/Empleados/RepositorioActualizarGrupoEmpleados'; + +/** + * * Hook para actualizar los datos de un grupo de empleados + * @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 {{ + * mensaje: string, + * * cargando: boolean, + * * error: string | null + * * }} + * * @see [RF[24] Actualizar grupo de empleados - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF24) + */ + +export const useActualizarGrupoEmpleados = ( + idGrupo, + nombre, + descripcion, + empleados, + setsDeProductos +) => { + const [mensaje, setMensaje] = useState(''); + const [cargando, setCargando] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const actualizarGrupoEmpleados = async () => { + setCargando(true); + setError(null); + + try { + const { mensaje } = await RepositorioActualizarGrupoEmpleados.actualizarGrupoEmpleados( + idGrupo, + nombre, + descripcion, + empleados, + setsDeProductos + ); + + setMensaje(mensaje); + } catch (err) { + setError(err.message); + } finally { + setCargando(false); + } + }; + + if (idGrupo) { + actualizarGrupoEmpleados(); + } + }, [idGrupo, nombre, descripcion, empleados, setsDeProductos]); + + return { mensaje, cargando, error }; +}; From 41ef2a9c375fba18332cc81ad22cc47b98842795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Antonio=20Ben=C3=ADtez=20De=20La=20Portilla?= Date: Tue, 13 May 2025 08:50:13 -0600 Subject: [PATCH 010/160] =?UTF-8?q?fix:=20correg=C3=AD=20los=20botones=20y?= =?UTF-8?q?=20al=20modal=20flotante=20lo=20puse=20en=20negritas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Vistas/Componentes/Moleculas/CuotasInfo.jsx | 5 ----- src/Vistas/Componentes/Organismos/ModalFlotante.jsx | 7 ++++++- src/Vistas/Paginas/Cuotas/ListaCuotas.jsx | 11 ++++++++++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Vistas/Componentes/Moleculas/CuotasInfo.jsx b/src/Vistas/Componentes/Moleculas/CuotasInfo.jsx index 87afe07f..33a2ef76 100644 --- a/src/Vistas/Componentes/Moleculas/CuotasInfo.jsx +++ b/src/Vistas/Componentes/Moleculas/CuotasInfo.jsx @@ -18,11 +18,6 @@ const CuotasInfo = ({ - - - Información de la Cuota - - Nombre:{' '} diff --git a/src/Vistas/Componentes/Organismos/ModalFlotante.jsx b/src/Vistas/Componentes/Organismos/ModalFlotante.jsx index be03191a..f2868bbb 100644 --- a/src/Vistas/Componentes/Organismos/ModalFlotante.jsx +++ b/src/Vistas/Componentes/Organismos/ModalFlotante.jsx @@ -65,7 +65,12 @@ const ModalFlotante = ({ }} > {titulo && ( - + {titulo} )} diff --git a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx index fde4e5ce..1e6e69a1 100644 --- a/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx +++ b/src/Vistas/Paginas/Cuotas/ListaCuotas.jsx @@ -191,7 +191,16 @@ const ListaCuotas = () => { setModalAbierto(false)} - titulo='Detalles del Set de Cuotas' + titulo='Detalles de la Cuota' + botones={[ + { + label: 'Cerrar', + variant: 'contained', + color: 'error', + backgroundColor: colores.altertex[1], + onClick: () => setModalAbierto(false), + }, + ]} > {cargandoDetalle || !cuota ? Cargando... : } From eddd7778e314a9f3966c472fce9a65bbb39e7115 Mon Sep 17 00:00:00 2001 From: NicoH00d Date: Thu, 22 May 2025 16:23:12 -0600 Subject: [PATCH 011/160] feature: Modal para importar productos --- .../Organismos/ModalImportarProductos.jsx | 195 ++++++++++++++++++ .../Paginas/Productos/ListaProductos.jsx | 23 ++- 2 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx diff --git a/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx b/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx new file mode 100644 index 00000000..c2e98ebb --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx @@ -0,0 +1,195 @@ +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 ContenedorImportar from '@Organismos/ContenedorImportar'; +import Alerta from '@Moleculas/Alerta'; +import { tokens } from '@SRC/theme'; +import InfoImportar from '@Organismos/InfoImportar'; +import CajaDesplazable from '@Organismos/CajaDesplazable'; + +const ModalImportarProductos = ({ abierto, onCerrar, onConfirm, cargando, errores, exito, recargar }) => { + const [archivo, setFile] = useState(null); + const [estado, setEstado] = useState('idle'); + const [infoJson, setInfoJson] = useState([]); + const [alerta, setAlerta] = useState(null); + const theme = useTheme(); + const colores = tokens(theme.palette.mode); + const [abririnfo, setAbrirInfo] = useState(false); + const [mensajeErrores, setMensajeErrores] = useState(''); + + // Manejo de errores en la importación + useEffect(() => { + if (errores && errores.length > 0) { + const mensaje = errores + .map(elemento => `Fila ${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([]); + setFile(null); + setEstado('idle'); + onCerrar(false); + setMensajeErrores(''); + } + }, [exito, onCerrar, recargar]); + + // Manejo de cierre del modal de importar + const handleCerrar = useCallback(() => { + onCerrar(false); + setInfoJson([]); + setFile(null); + setEstado('idle'); + setAlerta(null); + setMensajeErrores(''); + }, [onCerrar]); + + // Manejo de archivo aceptados + const handleFileAccepted = (archivo, data) => { + setFile(archivo); + setEstado('loading'); + setInfoJson(data); + setEstado('complete'); + }; + + // Manejo de eliminación de archivo + const handleEliminar = () => { + setFile(null); + setEstado('idle'); + setInfoJson([]); + }; + + // Manejo de confirmación de importación + const handleConfirmar = () => { + if (!infoJson.length) { + setAlerta({ + tipo: 'error', + mensaje: 'Agrega un archivo CSV válido.', + duracion: 2500, + cerrable: true, + centradoInferior: true, + }); + return; + } + onConfirm(infoJson); + setFile(null); + setEstado('idle'); + setAlerta(null); + setInfoJson([]); + + }; + + return ( + <> + + + + + {archivo && ( + + + } + > + + {estado === 'Cargando...' ? ( + + ) : ( + + )} + + + + + )} + + + + 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).
+
+

+ {mensajeErrores && ( + + + + {mensajeErrores} + + + + )} +
+ + + {alerta && ( + setAlerta(null)} + /> + )} + + + ); +}; + +export default ModalImportarProductos; diff --git a/src/Vistas/Paginas/Productos/ListaProductos.jsx b/src/Vistas/Paginas/Productos/ListaProductos.jsx index c65ed8ed..2dd807fc 100644 --- a/src/Vistas/Paginas/Productos/ListaProductos.jsx +++ b/src/Vistas/Paginas/Productos/ListaProductos.jsx @@ -14,6 +14,8 @@ 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'; + const ListaProductos = () => { const { productos, cargando, error, recargar } = useConsultarProductos(); const { eliminar } = useEliminarProductos(); @@ -29,6 +31,8 @@ const ListaProductos = () => { const [mostrarModalProducto, setMostrarModalProducto] = useState(false); const [alerta, setAlerta] = useState(null); const [openModalEliminar, setAbrirPopUp] = useState(false); + const [modalImportarAbierto, setModalImportarAbierto] = useState(false); + const mostrarFormularioProducto = useCallback(() => { setMostrarModalProducto(true); @@ -83,6 +87,8 @@ const ListaProductos = () => { } }; + const handleAbrirImportar = () => setModalImportarAbierto(true); + const columnas = [ { field: 'imagen', @@ -145,13 +151,13 @@ const ListaProductos = () => { backgroundColor: colores.altertex[1], }, { - //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], + }, { //variant: 'outlined', @@ -239,6 +245,15 @@ const ListaProductos = () => { confirmar={manejarConfirmarEliminar} dialogo={MENSAJE_POPUP_ELIMINAR} /> + setModalImportarAbierto(false)} + // onConfirm={importar} + // cargando={cargandoImportacion} + // errores={errores} + // exito={exito} + recargar={recargar} + > ); }; From 91490fd3c5cabba2ada9ddf68bf9cf516f4ba703 Mon Sep 17 00:00:00 2001 From: DiegoGarciaPadilla Date: Sun, 25 May 2025 10:26:16 -0600 Subject: [PATCH 012/160] feat: modal para crear un evento --- src/Vistas/Componentes/Atomos/CampoSelect.jsx | 2 + .../Organismos/Eventos/ModalCrearEvento.jsx | 101 +++++++++++++ .../Formularios/FormularioCrearEvento.jsx | 133 ++++++++++++++++++ src/Vistas/Paginas/Eventos/ListaEventos.jsx | 40 +++++- 4 files changed, 269 insertions(+), 7 deletions(-) create mode 100644 src/Vistas/Componentes/Organismos/Eventos/ModalCrearEvento.jsx create mode 100644 src/Vistas/Componentes/Organismos/Formularios/FormularioCrearEvento.jsx 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'} + +
+ ); +}; + +export default ContenedorImportarProductos; diff --git a/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx b/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx index c2e98ebb..328f4f5b 100644 --- a/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx +++ b/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx @@ -2,11 +2,11 @@ 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 ContenedorImportar from '@Organismos/ContenedorImportar'; import Alerta from '@Moleculas/Alerta'; import { tokens } from '@SRC/theme'; import InfoImportar from '@Organismos/InfoImportar'; import CajaDesplazable from '@Organismos/CajaDesplazable'; +import ContenedorImportarProductos from './ContenedorImportarProductos'; const ModalImportarProductos = ({ abierto, onCerrar, onConfirm, cargando, errores, exito, recargar }) => { const [archivo, setFile] = useState(null); @@ -99,7 +99,7 @@ const ModalImportarProductos = ({ abierto, onCerrar, onConfirm, cargando, errore <> - + {archivo && ( @@ -147,21 +147,13 @@ const ModalImportarProductos = ({ 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).
+ Under Construction

{mensajeErrores && ( diff --git a/src/Vistas/Paginas/Productos/ListaProductos.jsx b/src/Vistas/Paginas/Productos/ListaProductos.jsx index 2dd807fc..d1698953 100644 --- a/src/Vistas/Paginas/Productos/ListaProductos.jsx +++ b/src/Vistas/Paginas/Productos/ListaProductos.jsx @@ -15,6 +15,7 @@ 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'; const ListaProductos = () => { const { productos, cargando, error, recargar } = useConsultarProductos(); @@ -32,6 +33,8 @@ const ListaProductos = () => { const [alerta, setAlerta] = useState(null); const [openModalEliminar, setAbrirPopUp] = useState(false); const [modalImportarAbierto, setModalImportarAbierto] = useState(false); + const { importar, errores, exito, cargando: cargandoImportacion } = useImportarProductos(); + const mostrarFormularioProducto = useCallback(() => { @@ -248,10 +251,10 @@ const ListaProductos = () => { setModalImportarAbierto(false)} - // onConfirm={importar} - // cargando={cargandoImportacion} - // errores={errores} - // exito={exito} + onConfirm={importar} + cargando={cargandoImportacion} + errores={errores} + exito={exito} recargar={recargar} > diff --git a/src/hooks/Productos/useImportarProductos.js b/src/hooks/Productos/useImportarProductos.js new file mode 100644 index 00000000..cbf81305 --- /dev/null +++ b/src/hooks/Productos/useImportarProductos.js @@ -0,0 +1,49 @@ +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; From 4a34967cd7256698ba2a77a8ed41a4619388984d Mon Sep 17 00:00:00 2001 From: NicoH00d Date: Tue, 27 May 2025 22:37:22 -0600 Subject: [PATCH 037/160] feature: Ajustar renderCell en lista productos para imagen de placeholder --- src/Vistas/Paginas/Productos/ListaProductos.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Vistas/Paginas/Productos/ListaProductos.jsx b/src/Vistas/Paginas/Productos/ListaProductos.jsx index fcefef8e..2e65ab07 100644 --- a/src/Vistas/Paginas/Productos/ListaProductos.jsx +++ b/src/Vistas/Paginas/Productos/ListaProductos.jsx @@ -99,7 +99,7 @@ const ListaProductos = () => { flex: 0.5, renderCell: (params) => ( Producto From 28a3889a22696b06822f07680328747fc36f7ccf Mon Sep 17 00:00:00 2001 From: Hiram <147564077+Hiram10tec@users.noreply.github.com> Date: Wed, 28 May 2025 11:17:16 -0600 Subject: [PATCH 038/160] =?UTF-8?q?feature(categorias):=20avance=20sin=20f?= =?UTF-8?q?uncionalidad=20completa=20de=20actualizar=20categor=C3=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repositorioActualizarCategoria.js | 26 +++++ src/Utilidades/Constantes/rutasAPI.js | 1 + .../Formularios/FormaCrearCategoria.jsx | 1 + .../Organismos/ModalActualizarCategoria.jsx | 95 +++++++++++++++++++ .../Paginas/Categorias/ListaCategorias.jsx | 21 +++- .../Categorias/useActualizarCategoria.js | 28 ++++++ src/hooks/Categorias/useLeerCategoria.js | 1 + 7 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/Dominio/Repositorios/Categorias/repositorioActualizarCategoria.js create mode 100644 src/Vistas/Componentes/Organismos/ModalActualizarCategoria.jsx create mode 100644 src/hooks/Categorias/useActualizarCategoria.js diff --git a/src/Dominio/Repositorios/Categorias/repositorioActualizarCategoria.js b/src/Dominio/Repositorios/Categorias/repositorioActualizarCategoria.js new file mode 100644 index 00000000..f0123701 --- /dev/null +++ b/src/Dominio/Repositorios/Categorias/repositorioActualizarCategoria.js @@ -0,0 +1,26 @@ +// RF48 Actualizar Categoría - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF48 +import axios from 'axios'; +import { RUTAS_API } from '@Constantes/rutasAPI'; + +const API_KEY = import.meta.env.VITE_API_KEY; + +export class RepositorioActualizarCategoria { + static async actualizar(idCategoria, cambios) { + try { + const respuesta = await axios.put( + `${RUTAS_API.CATEGORIAS.ACTUALIZAR}/${idCategoria}`, + cambios, + { + 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/Utilidades/Constantes/rutasAPI.js b/src/Utilidades/Constantes/rutasAPI.js index c56930d9..019f3279 100644 --- a/src/Utilidades/Constantes/rutasAPI.js +++ b/src/Utilidades/Constantes/rutasAPI.js @@ -34,6 +34,7 @@ export const RUTAS_API = { CREAR: `${BASE_CATEGORIAS}/crear-categoria`, ELIMINAR_CATEGORIA: `${BASE_CATEGORIAS}/eliminar`, LEER: `${BASE_CATEGORIAS}/leer`, + ACTUALIZAR: `${BASE_CATEGORIAS}/actualizar`, }, PRODUCTOS: { BASE: BASE_PRODUCTOS, diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx index bedb3979..44b119e2 100644 --- a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx +++ b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx @@ -32,6 +32,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/ModalActualizarCategoria.jsx b/src/Vistas/Componentes/Organismos/ModalActualizarCategoria.jsx new file mode 100644 index 00000000..22567a7f --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalActualizarCategoria.jsx @@ -0,0 +1,95 @@ +// 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((p) => + typeof p === 'object' ? p.idProducto : p + ); + 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/Paginas/Categorias/ListaCategorias.jsx b/src/Vistas/Paginas/Categorias/ListaCategorias.jsx index bbc50f58..ad9527c8 100644 --- a/src/Vistas/Paginas/Categorias/ListaCategorias.jsx +++ b/src/Vistas/Paginas/Categorias/ListaCategorias.jsx @@ -11,6 +11,7 @@ import { leerCategoria } from '@Hooks/Categorias/useLeerCategoria'; import { Box, useTheme } from '@mui/material'; import Texto from '@Atomos/Texto'; import { tokens } from '@SRC/theme'; +import ModalActualizarCategoria from '@Organismos/ModalActualizarCategoria'; const ListaCategorias = () => { const { categorias, cargando, error, recargar } = useConsultarCategorias(); @@ -23,6 +24,8 @@ const ListaCategorias = () => { const [categoriaDetalle, setCategoriaDetalle] = useState(null); const [errorDetalle, setErrorDetalle] = useState(false); const [cargandoDetalle, setCargandoDetalle] = useState(false); + const [modalActualizarAbierto, setModalActualizarAbierto] = useState(false); + const [categoriaSeleccionada, setCategoriaSeleccionada] = useState(null); const theme = useTheme(); const colores = tokens(theme.palette.mode); @@ -189,7 +192,14 @@ const ListaCategorias = () => { variant: 'contained', color: 'error', backgroundColor: colores.altertex[1], - construccion: true, + onClick: () => { + setCategoriaSeleccionada({ + ...categoriaDetalle, + productos: categoriaDetalle.productos.map(p => p.idProducto), // para evitar errores en el formulario + }); + setModalDetalleAbierto(false); + setModalActualizarAbierto(true); + }, }, { label: 'SALIR', @@ -210,6 +220,15 @@ const ListaCategorias = () => { )} + {modalActualizarAbierto && categoriaSeleccionada && ( + setModalActualizarAbierto(false)} + categoria={categoriaSeleccionada} + onActualizado={recargar} + /> + )} + {alerta && ( { + setCargando(true); + setError(null); + setMensaje(''); + + try { + const respuesta = await RepositorioActualizarCategoria.actualizar(idCategoria, cambios); + setMensaje(respuesta.mensaje || 'Categoría actualizada correctamente'); + return true; + } catch (err) { + setError(err.message || 'Ocurrió un error al actualizar'); + return false; + } finally { + setCargando(false); + } + }; + + return { actualizar, cargando, error, mensaje }; +} \ No newline at end of file diff --git a/src/hooks/Categorias/useLeerCategoria.js b/src/hooks/Categorias/useLeerCategoria.js index 64f8bd1e..c55a9e76 100644 --- a/src/hooks/Categorias/useLeerCategoria.js +++ b/src/hooks/Categorias/useLeerCategoria.js @@ -22,6 +22,7 @@ export const leerCategoria = async (idCategoria) => { const productos = data.categoria.productos?.map((produc) => produc.nombreComun) || []; return { + idCategoria: data.categoria.idCategoria, nombreCategoria: data.categoria.nombreCategoria, descripcion: data.categoria.descripcion, productos, From dd3cc142695aad8e62ca09fe15e06e0bc6d24ce7 Mon Sep 17 00:00:00 2001 From: NicoH00d Date: Wed, 28 May 2025 11:26:22 -0600 Subject: [PATCH 039/160] feat: validar campos --- .../Organismos/ContenedorImportar.jsx | 6 +++--- .../Organismos/ContenedorImportarProductos.jsx | 3 +-- .../Organismos/ModalImportarEmpleados.jsx | 2 +- .../Organismos/ModalImportarProductos.jsx | 16 ++++++++++++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Vistas/Componentes/Organismos/ContenedorImportar.jsx b/src/Vistas/Componentes/Organismos/ContenedorImportar.jsx index 82649d01..3a4b6952 100644 --- a/src/Vistas/Componentes/Organismos/ContenedorImportar.jsx +++ b/src/Vistas/Componentes/Organismos/ContenedorImportar.jsx @@ -34,10 +34,10 @@ const ContenedorImportar = ({ onFileAccepted, onError, cargando = false }) => { if (error?.code === 'file-too-large') { const sizeMB = (file?.size || 0) / (1024 * 1024); - const mensaje = `⚠️ El archivo "${file.name}" es muy grande (${sizeMB.toFixed(2)} MB).`; + const mensaje = `El archivo "${file.name}" es muy grande (${sizeMB.toFixed(2)} MB).`; onError?.(mensaje); } else if (error?.code === 'file-invalid-type') { - onError?.(`⚠️ El archivo debe ser de tipo .csv`); + onError?.(`El archivo debe ser de tipo .csv`); } else { onError?.(`${error?.message || 'Archivo no aceptado.'}`); } @@ -76,7 +76,7 @@ const ContenedorImportar = ({ onFileAccepted, onError, cargando = false }) => { ? 'Cargando…' : isDragActive ? 'Suelta el archivo aquí…' - : 'Arrastra tu CSV o haz clic para seleccionarlo'} + : 'Arrastra tu CSV o haz clic para seleccionarlo (5 MB máximo)'} ); diff --git a/src/Vistas/Componentes/Organismos/ContenedorImportarProductos.jsx b/src/Vistas/Componentes/Organismos/ContenedorImportarProductos.jsx index c4c860c2..a7cd2435 100644 --- a/src/Vistas/Componentes/Organismos/ContenedorImportarProductos.jsx +++ b/src/Vistas/Componentes/Organismos/ContenedorImportarProductos.jsx @@ -108,7 +108,6 @@ const ContenedorImportarProductos = ({ onFileAccepted, onError, cargando = false complete: (resultado) => { try { const productosParseados = transformarCSVaEstructuraProductos(resultado.data); - console.log('🧪 Productos Parseados con ID proveedor:', productosParseados); onFileAccepted(archivo, productosParseados); } catch (error) { onError?.('Error al transformar los datos del CSV. Verifica el formato.'); @@ -166,7 +165,7 @@ const ContenedorImportarProductos = ({ onFileAccepted, onError, cargando = false ? 'Cargando…' : isDragActive ? 'Suelta el archivo aquí…' - : 'Arrastra tu CSV o haz clic para seleccionarlo'} + : 'Arrastra tu CSV o haz clic para seleccionarlo (5 MB máximo)'} ); diff --git a/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx b/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx index 90dad3d5..f267171b 100644 --- a/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx +++ b/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx @@ -160,7 +160,7 @@ const ModalImportarEmpleados = ({ abierto, onCerrar, onConfirm, cargando, errore - Descargar CSV de ejemplo + Descargar plantilla CSV - + + setAlerta({ + tipo: 'error', + mensaje, + duracion: 3000, + cerrable: true, + centradoInferior: true, + }) + } + /> {archivo && ( @@ -148,7 +160,7 @@ const ModalImportarProductos = ({ abierto, onCerrar, onConfirm, cargando, errore - Descargar CSV de ejemplo + Descargar plantilla CSV Date: Thu, 29 May 2025 01:38:28 -0600 Subject: [PATCH 040/160] fix: arreglar columnas para quitar los campos innecesarios --- src/Vistas/Paginas/Eventos/ListaEventos.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Vistas/Paginas/Eventos/ListaEventos.jsx b/src/Vistas/Paginas/Eventos/ListaEventos.jsx index bb0b0487..21ce8a5c 100644 --- a/src/Vistas/Paginas/Eventos/ListaEventos.jsx +++ b/src/Vistas/Paginas/Eventos/ListaEventos.jsx @@ -102,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) => ({ From 9b05e5101f98e971314cca17224b55fd82cd69c6 Mon Sep 17 00:00:00 2001 From: Diego Alfaro Pinto Date: Thu, 29 May 2025 15:11:20 -0600 Subject: [PATCH 041/160] fix arreglar alerta --- .../Organismos/ModalCrearSetsProductos.jsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Vistas/Componentes/Organismos/ModalCrearSetsProductos.jsx b/src/Vistas/Componentes/Organismos/ModalCrearSetsProductos.jsx index 050e93cb..6a5d0ed8 100644 --- a/src/Vistas/Componentes/Organismos/ModalCrearSetsProductos.jsx +++ b/src/Vistas/Componentes/Organismos/ModalCrearSetsProductos.jsx @@ -10,6 +10,7 @@ const ModalCrearSetsProductos = ({ abierto = false, onCerrar, onCreado }) => { const [descripcionSetsProducto, setDescripcionSetsProducto] = useState(''); const [productos, setProductos] = useState([]); const [mostrarAlerta, setMostrarAlerta] = useState(false); + const [mensajeAlerta, setMensajeAlerta] = useState('') const tieneReseteo = useRef(false); @@ -58,9 +59,14 @@ const ModalCrearSetsProductos = ({ abierto = false, onCerrar, onCreado }) => { const handleConfirmar = async () => { if (!nombreSetsProducto.trim() || !nombreVisible.trim() || productos.length === 0) { setMostrarAlerta(true); + if (productos.length === 0) { + setMensajeAlerta('Debe seleccionar al menos un producto.'); + } else { + setMensajeAlerta('Por favor complete todos los campos obligatorios.'); + } return; } - + setMensajeAlerta(''); await crearSetsProducto({ nombre: nombreSetsProducto.trim(), nombreVisible: nombreVisible.trim(), @@ -92,6 +98,19 @@ const ModalCrearSetsProductos = ({ abierto = false, onCerrar, onCreado }) => { setMostrarAlerta={setMostrarAlerta} /> + + {mostrarAlerta && ( + setMostrarAlerta(false)} + centradoInferior + /> + )} + {(exito || error) && ( Date: Fri, 30 May 2025 12:13:38 -0600 Subject: [PATCH 042/160] Fix: Manejo de estados --- .../Moleculas/GrupoEmpleadosInfoEditable.jsx | 55 ++++++++++++++----- .../Paginas/Empleados/ListaGrupoEmpleados.jsx | 2 + 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx index 09e94fd1..85f3e93a 100644 --- a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx +++ b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx @@ -12,7 +12,9 @@ const InfoGrupoEmpleadosEditable = ({ nombre: nombreInicial, descripcion: descripcionInicial, setsProductos: setsProductosInicial, + idsSetProductos: idsSetsProductosInicial, // IDs iniciales de sets de productos empleados: empleadosInicial, + idsEmpleados: idsEmpleadosInicial, // IDs iniciales de empleados }) => { const [productosDisponibles, setProductosDisponibles] = useState([]); const [empleadosDisponibles, setEmpleadosDisponibles] = useState([]); @@ -23,11 +25,14 @@ const InfoGrupoEmpleadosEditable = ({ const [nombre, setNombre] = useState(nombreInicial || ''); const [descripcion, setDescripcion] = useState(descripcionInicial || ''); const [setsProductos, setSetsProductos] = useState(setsProductosInicial || []); - const [empleados, setEmpleados] = useState(empleadosInicial); + const [empleados, setEmpleados] = useState(empleadosInicial || []); const [mostrarAlerta, setMostrarAlerta] = useState(false); - // Estados para manejar las selecciones en las tablas - const [productosSeleccionados, setProductosSeleccionados] = useState([]); - const [empleadosSeleccionados, setEmpleadosSeleccionados] = useState([]); + + // Estados para manejar las selecciones en las tablas - INICIALIZADOS CON LOS IDs + const [productosSeleccionados, setProductosSeleccionados] = useState( + idsSetsProductosInicial || [] + ); + const [empleadosSeleccionados, setEmpleadosSeleccionados] = useState(idsEmpleadosInicial || []); useEffect(() => { const obtenerDatos = async () => { @@ -41,16 +46,37 @@ const InfoGrupoEmpleadosEditable = ({ obtenerDatos(); }, [clienteSeleccionado]); - // Sincronizar selecciones con los chips cuando cambian los datos + // Efecto para sincronizar cuando cambien los IDs iniciales (por si se recarga el componente) + useEffect(() => { + if (idsSetsProductosInicial && idsSetsProductosInicial.length > 0) { + setProductosSeleccionados(idsSetsProductosInicial); + } + }, [idsSetsProductosInicial]); + + useEffect(() => { + if (idsEmpleadosInicial && idsEmpleadosInicial.length > 0) { + setEmpleadosSeleccionados(idsEmpleadosInicial); + } + }, [idsEmpleadosInicial]); + + // Efecto para mantener sincronizados los chips con las selecciones useEffect(() => { - const idsProductosSeleccionados = setsProductos.map((producto) => producto.id); - setProductosSeleccionados(idsProductosSeleccionados); - }, [setsProductos]); + if (productosDisponibles.length > 0 && productosSeleccionados.length > 0) { + const productosActualizados = productosDisponibles.filter((producto) => + productosSeleccionados.includes(producto.id) + ); + setSetsProductos(productosActualizados); + } + }, [productosDisponibles, productosSeleccionados]); useEffect(() => { - const idsEmpleadosSeleccionados = empleados.map((empleado) => empleado.id); - setEmpleadosSeleccionados(idsEmpleadosSeleccionados); - }, [empleados]); + if (empleadosDisponibles.length > 0 && empleadosSeleccionados.length > 0) { + const empleadosActualizados = empleadosDisponibles.filter((empleado) => + empleadosSeleccionados.includes(empleado.id) + ); + setEmpleados(empleadosActualizados); + } + }, [empleadosDisponibles, empleadosSeleccionados]); // Manejar cambios en la selección de productos const handleSeleccionProductos = (selectionData) => { @@ -75,8 +101,6 @@ const InfoGrupoEmpleadosEditable = ({ // Manejar cambios en la selección de empleados const handleSeleccionEmpleados = (selectionData) => { console.log('Selecciones empleados recibidas:', selectionData); - - // Extraer IDs del Set y convertir a array let seleccionesArray = []; if (selectionData && selectionData.ids && selectionData.ids instanceof Set) { seleccionesArray = Array.from(selectionData.ids); @@ -100,8 +124,9 @@ const InfoGrupoEmpleadosEditable = ({ correo: empleado.correo, areaTrabajo: empleado.area, })); - console.log('Produtos seleccionados:', productosSeleccionados); + console.log('Productos seleccionados:', productosSeleccionados); console.log('Empleados seleccionados:', empleadosSeleccionados); + const handleGuardar = () => { if (!nombre || !descripcion || setsProductos.length === 0 || empleados.length === 0) { setMostrarAlerta(true); @@ -112,6 +137,8 @@ const InfoGrupoEmpleadosEditable = ({ console.log('Descripción:', descripcion); console.log('Sets de Productos:', setsProductos); console.log('Empleados:', empleados); + console.log('IDs Sets Productos:', productosSeleccionados); + console.log('IDs Empleados:', empleadosSeleccionados); }; return ( diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index 112a0ec1..ea2f7bd2 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -269,7 +269,9 @@ const ListaGrupoEmpleados = () => { nombre={grupoEmpleados?.nombre || ''} descripcion={grupoEmpleados?.descripcion || ''} setsProductos={grupoEmpleados?.setsProductos || []} + idsSetProductos={grupoEmpleados?.idsSetProductos || []} empleados={grupoEmpleados?.empleados || []} + idsEmpleados={grupoEmpleados?.idsEmpleados || []} /> )} From 63d19c49219bdd345992a5fa795ab0f73285367c Mon Sep 17 00:00:00 2001 From: PAOLA MARIA garrido Date: Fri, 30 May 2025 18:42:14 -0600 Subject: [PATCH 043/160] =?UTF-8?q?fix:=20agregar=20exportar=20por=20selec?= =?UTF-8?q?ci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Empleados/RepositorioExportarEmpleados.js | 30 +++++++ src/Utilidades/Constantes/rutasAPI.js | 1 + .../Paginas/Empleados/ListaEmpleados.jsx | 89 +++++++++++++++++-- src/hooks/Empleados/useExportarEmpleados.js | 41 +++++++++ 4 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 src/Dominio/Repositorios/Empleados/RepositorioExportarEmpleados.js create mode 100644 src/hooks/Empleados/useExportarEmpleados.js diff --git a/src/Dominio/Repositorios/Empleados/RepositorioExportarEmpleados.js b/src/Dominio/Repositorios/Empleados/RepositorioExportarEmpleados.js new file mode 100644 index 00000000..0d36dfb8 --- /dev/null +++ b/src/Dominio/Repositorios/Empleados/RepositorioExportarEmpleados.js @@ -0,0 +1,30 @@ +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. + */ + +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/Utilidades/Constantes/rutasAPI.js b/src/Utilidades/Constantes/rutasAPI.js index ea8837c0..d2856b75 100644 --- a/src/Utilidades/Constantes/rutasAPI.js +++ b/src/Utilidades/Constantes/rutasAPI.js @@ -69,6 +69,7 @@ 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`, }, CUOTAS: { BASE: BASE_CUOTAS, diff --git a/src/Vistas/Paginas/Empleados/ListaEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaEmpleados.jsx index d962012c..53e0ac34 100644 --- a/src/Vistas/Paginas/Empleados/ListaEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaEmpleados.jsx @@ -1,6 +1,6 @@ //RF17 - Consulta Lista Empleados - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF17 //RF20 - Eliminar empleado - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF20 -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Box, useTheme } from '@mui/material'; import Tabla from '@Organismos/Tabla'; import ContenedorLista from '@Organismos/ContenedorLista'; @@ -16,6 +16,7 @@ import { tokens } from '@SRC/theme'; import { PERMISOS } from '@Constantes/permisos'; import ModalImportarEmpleados from '@Organismos/ModalImportarEmpleados'; import useImportarEmpleados from '@Hooks/Empleados/useImportarEmpleados'; +import useExportarEmpleados from '@Hooks/Empleados/useExportarEmpleados'; const ListaGrupoEmpleados = () => { const { empleados, cargando, error, recargar } = useConsultarEmpleados(); @@ -45,7 +46,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 +68,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); // ✅ Pasa los IDs seleccionados + 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 }, @@ -109,13 +172,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 +222,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); }} /> @@ -241,6 +305,15 @@ const ListaGrupoEmpleados = () => { dialogo={MENSAJE_POPUP_ELIMINAR} /> + + {/* Alerta inferior */} {alerta && ( { ); }; -export default ListaGrupoEmpleados; +export default ListaGrupoEmpleados; \ No newline at end of file diff --git a/src/hooks/Empleados/useExportarEmpleados.js b/src/hooks/Empleados/useExportarEmpleados.js new file mode 100644 index 00000000..2c7d5c28 --- /dev/null +++ b/src/hooks/Empleados/useExportarEmpleados.js @@ -0,0 +1,41 @@ +import { useState, useEffect } 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 + * }} + */ +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 From 6dec5f15695955890b5fd75d9136b666b277cd9a Mon Sep 17 00:00:00 2001 From: NicoH00d Date: Fri, 30 May 2025 19:00:11 -0600 Subject: [PATCH 044/160] feat: agregar documentacion para importar --- .../Organismos/ModalImportarEmpleados.jsx | 18 ++++----- .../Organismos/ModalImportarProductos.jsx | 39 ++++++++++++++++++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx b/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx index f267171b..f2c256ae 100644 --- a/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx +++ b/src/Vistas/Componentes/Organismos/ModalImportarEmpleados.jsx @@ -165,15 +165,15 @@ const ModalImportarEmpleados = ({ abierto, onCerrar, onConfirm, cargando, errore 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 index cc4aed37..6a72c9c3 100644 --- a/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx +++ b/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx @@ -22,7 +22,7 @@ const ModalImportarProductos = ({ abierto, onCerrar, onConfirm, cargando, errore useEffect(() => { if (errores && errores.length > 0) { const mensaje = errores - .map(elemento => `Fila ${elemento.fila}: ${elemento.error}`) + .map(elemento => `Producto ${elemento.fila}: ${elemento.error}`) .join('\n'); setMensajeErrores(mensaje); } else { @@ -165,7 +165,42 @@ const ModalImportarProductos = ({ abierto, onCerrar, onConfirm, cargando, errore setAbrirInfo(false)}> - Under Construction + <> + ¿Cómo funciona la estructura del archivo CSV? +

+ Cada fila del archivo representa una opción específica de un producto. Por eso, en cada fila se debe repetir la información general del 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. +

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

+ Importante:
+ Asegúrate de que 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. +

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.
+ nombreProducto, nombreComercial: Nombres básicos
+ descripcionProducto: Descripción del producto
+ marca, modelo, tipoProducto
+ costo, precioVenta, precioCliente: Valores numéricos (usa punto decimal)
+ precioPuntos: Número entero
+ impuesto, descuento: Porcentajes
+ estado: 1 = activo, 0 = inactivo
+ envio: 1 = disponible, 0 = no disponible

+

Variante

+ nombreVariante: Ej. "Color", "Tamaño"
+ descripcionVariante: (opcional)

+

Opción

+ valorOpcion: Ej. "Rojo", "XL"
+ SKUautomatico: Obligatorio
+ SKUcomercial: Código visible al cliente
+ cantidad: Entero
+ costoAdicional: Número positivo
+ descuentoOpcion: Porcentaje
+ estadoOpcion: 1 = activa, 0 = inactiva

+ +

{mensajeErrores && ( From 55bb344bdb300fb1a70fc786f8c249683399da3c Mon Sep 17 00:00:00 2001 From: Diego Alfaro Pinto Date: Sat, 31 May 2025 20:24:26 -0600 Subject: [PATCH 045/160] fix: arreglar manejo de errores segun los errores encontrados en las pruebas --- .../Formularios/FormaCrearSetsProducto.jsx | 128 ++++++++++++++---- .../Organismos/ModalCrearSetsProductos.jsx | 107 ++++++++++----- 2 files changed, 173 insertions(+), 62 deletions(-) diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearSetsProducto.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearSetsProducto.jsx index d19c309a..1d9ca7a0 100644 --- a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearSetsProducto.jsx +++ b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearSetsProducto.jsx @@ -27,6 +27,8 @@ const FormaCrearSetsProducto = ({ setProductos, mostrarAlerta, setMostrarAlerta, + erroresCampos, + setErroresCampos, }) => { const [rows, setRows] = useState([]); const { usuario } = useAuth(); @@ -47,18 +49,73 @@ const FormaCrearSetsProducto = ({ const yaExiste = productos.some((producto) => producto.id === productoSeleccionado.id); if (!yaExiste) { setProductos((prev) => [...prev, productoSeleccionado]); + } else { + // Remove product if it already exists (toggle behavior) + setProductos((prev) => prev.filter((producto) => producto.id !== productoSeleccionado.id)); + } + + // Clear products error when a product is selected + if (erroresCampos?.productos) { + setErroresCampos(prev => ({ ...prev, productos: false })); } }; const handleFilaSeleccion = (itemSeleccion) => { const ids = Array.isArray(itemSeleccion) ? itemSeleccion : Array.from(itemSeleccion?.ids || []); - const nuevasFilas = ids + // Get all products that correspond to the selected IDs + const productosSeleccionados = ids .map((id) => rows.find((row) => row.id === id)) - .filter((fila) => fila && !productos.some((producto) => producto.id === fila.id)); + .filter((fila) => fila); + + // Update the productos array to match exactly what's selected + setProductos(productosSeleccionados); + + // Clear products error when products are selected + 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(''); + } + + // Clear error when user starts typing + 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 (nuevasFilas.length > 0) { - setProductos((prev) => [...prev, ...nuevasFilas]); + // Clear error when user starts typing + 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(''); + } + + // Clear error when user starts typing + if (erroresCampos?.descripcion && value.trim()) { + setErroresCampos(prev => ({ ...prev, descripcion: false })); } }; @@ -69,17 +126,19 @@ const FormaCrearSetsProducto = ({ fullWidth type={'text'} value={nombreSetsProducto} - onChange={(evento) => { - if (evento.target.value.trim() !== '') { - setNombreSetsProducto(evento.target.value.slice(0, LIMITE_NOMBRE)); - } else if (nombreSetsProducto !== '') { - setNombreSetsProducto(''); - } - }} + onChange={handleNombreChange} inputProps={{ maxLength: LIMITE_NOMBRE }} helperText={`${nombreSetsProducto.length}/${LIMITE_NOMBRE} - ${MENSAJE_LIMITE}`} required - sx={{ mb: 2 }} + error={erroresCampos?.nombre} + sx={{ + mb: 2, + '& .MuiOutlinedInput-root': { + '&.Mui-error .MuiOutlinedInput-notchedOutline': { + borderColor: '#f44336', + }, + }, + }} /> { - if (evento.target.value.trim() !== '') { - setNombreVisible(evento.target.value.slice(0, LIMITE_NOMBRE_VISIBLE)); - } else if (nombreVisible !== '') { - setNombreVisible(''); - } - }} + onChange={handleNombreVisibleChange} inputProps={{ maxLength: LIMITE_NOMBRE_VISIBLE }} helperText={`${nombreVisible.length}/${LIMITE_NOMBRE_VISIBLE} - ${MENSAJE_LIMITE}`} required - sx={{ mb: 2 }} + error={erroresCampos?.nombreVisible} + sx={{ + mb: 2, + '& .MuiOutlinedInput-root': { + '&.Mui-error .MuiOutlinedInput-notchedOutline': { + borderColor: '#f44336', + }, + }, + }} /> { - if (evento.target.value.trim() !== '') { - setDescripcionSetsProducto(evento.target.value.slice(0, LIMITE_DESCRIPCION)); - } else if (descripcionSetsProducto !== '') { - setDescripcionSetsProducto(''); - } - }} + onChange={handleDescripcionChange} inputProps={{ maxLength: LIMITE_DESCRIPCION }} helperText={`${descripcionSetsProducto.length}/${LIMITE_DESCRIPCION} - ${MENSAJE_LIMITE}`} required - sx={{ mt: 2 }} + error={erroresCampos?.descripcion} + sx={{ + mt: 2, + '& .MuiOutlinedInput-root': { + '&.Mui-error .MuiOutlinedInput-notchedOutline': { + borderColor: '#f44336', + }, + }, + }} multiline rows={3} /> diff --git a/src/Vistas/Componentes/Organismos/ModalCrearSetsProductos.jsx b/src/Vistas/Componentes/Organismos/ModalCrearSetsProductos.jsx index 6a5d0ed8..e5e0d410 100644 --- a/src/Vistas/Componentes/Organismos/ModalCrearSetsProductos.jsx +++ b/src/Vistas/Componentes/Organismos/ModalCrearSetsProductos.jsx @@ -10,7 +10,13 @@ const ModalCrearSetsProductos = ({ abierto = false, onCerrar, onCreado }) => { const [descripcionSetsProducto, setDescripcionSetsProducto] = useState(''); const [productos, setProductos] = useState([]); const [mostrarAlerta, setMostrarAlerta] = useState(false); - const [mensajeAlerta, setMensajeAlerta] = useState('') + const [mensajeAlerta, setMensajeAlerta] = useState(''); + const [erroresCampos, setErroresCampos] = useState({ + nombre: false, + nombreVisible: false, + descripcion: false, + productos: false + }); const tieneReseteo = useRef(false); @@ -27,6 +33,12 @@ const ModalCrearSetsProductos = ({ abierto = false, onCerrar, onCreado }) => { setDescripcionSetsProducto(''); setProductos([]); setMostrarAlerta(false); + setErroresCampos({ + nombre: false, + nombreVisible: false, + descripcion: false, + productos: false + }); }, 0); } else if (abierto) { tieneReseteo.current = false; @@ -56,17 +68,44 @@ const ModalCrearSetsProductos = ({ abierto = false, onCerrar, onCreado }) => { 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 () => { - if (!nombreSetsProducto.trim() || !nombreVisible.trim() || productos.length === 0) { + const { tieneErrores, mensaje } = validarCampos(); + + if (tieneErrores) { setMostrarAlerta(true); - if (productos.length === 0) { - setMensajeAlerta('Debe seleccionar al menos un producto.'); - } else { - setMensajeAlerta('Por favor complete todos los campos obligatorios.'); - } + setMensajeAlerta(mensaje); return; } + + setMostrarAlerta(false); setMensajeAlerta(''); + setErroresCampos({ + nombre: false, + nombreVisible: false, + descripcion: false, + productos: false + }); + await crearSetsProducto({ nombre: nombreSetsProducto.trim(), nombreVisible: nombreVisible.trim(), @@ -76,34 +115,36 @@ const ModalCrearSetsProductos = ({ abierto = false, onCerrar, onCreado }) => { }; return (<> - - - - - {mostrarAlerta && ( + + + + + {mostrarAlerta && !exito && !error && ( setMostrarAlerta(false)} @@ -115,7 +156,7 @@ const ModalCrearSetsProductos = ({ abierto = false, onCerrar, onCreado }) => { setError(false) : undefined} From 6a51b83604192a13c09a5c4fbaa8318ed6f8eb4a Mon Sep 17 00:00:00 2001 From: angieriosc Date: Sun, 1 Jun 2025 15:52:18 -0600 Subject: [PATCH 046/160] Feat: manejar la seleccion de empleados y sets con una lista de transferencia --- .../Empleados/GrupoEmpleadosLectura.js | 4 + .../Moleculas/GrupoEmpleadosInfoEditable.jsx | 516 ++++++++++++------ .../Paginas/Empleados/ListaGrupoEmpleados.jsx | 6 +- 3 files changed, 364 insertions(+), 162 deletions(-) diff --git a/src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js b/src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js index efc00983..4f16ced8 100644 --- a/src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js +++ b/src/Dominio/Modelos/Empleados/GrupoEmpleadosLectura.js @@ -12,6 +12,8 @@ export class GrupoEmpleadosLectura { idsSetProductos = [], empleados = [], idsEmpleados = [], + empleadosActualizar = [], + setProductosActualizar = [], }) { this.idGrupo = idGrupo; this.nombre = nombre; @@ -20,5 +22,7 @@ export class GrupoEmpleadosLectura { this.idsSetProductos = idsSetProductos; this.empleados = empleados; this.idsEmpleados = idsEmpleados; + this.empleadosActualizar = empleadosActualizar; + this.setProductosActualizar = setProductosActualizar; } } diff --git a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx index 85f3e93a..2fa55790 100644 --- a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx +++ b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx @@ -3,18 +3,40 @@ import CampoTexto from '@Atomos/CampoTexto'; import { useState, useEffect } from 'react'; import obtenerSetsProductos from '@Servicios/obtenerSetsProductos'; import obtenerEmpleados from '@Servicios/obtenerEmpleados'; -import TablaSetsEmpleados from '@Organismos/TablaSetsEmpleados'; import { useAuth } from '@Hooks/AuthProvider'; -import { Box, Button, Chip, Grid } from '@mui/material'; +import { + Box, + Button, + Grid, + Card, + CardHeader, + List, + ListItemButton, + ListItemIcon, + ListItemText, + Checkbox, + Divider, +} from '@mui/material'; import Texto from '@Atomos/Texto'; +// Funciones auxiliares para la lista de transferencia +function not(a, b) { + return a.filter((value) => !b.find((item) => item.id === value.id)); +} + +function intersection(a, b) { + return a.filter((value) => b.find((item) => item.id === value.id)); +} + +function union(a, b) { + return [...a, ...not(b, a)]; +} + const InfoGrupoEmpleadosEditable = ({ nombre: nombreInicial, descripcion: descripcionInicial, setsProductos: setsProductosInicial, - idsSetProductos: idsSetsProductosInicial, // IDs iniciales de sets de productos empleados: empleadosInicial, - idsEmpleados: idsEmpleadosInicial, // IDs iniciales de empleados }) => { const [productosDisponibles, setProductosDisponibles] = useState([]); const [empleadosDisponibles, setEmpleadosDisponibles] = useState([]); @@ -24,128 +46,262 @@ const InfoGrupoEmpleadosEditable = ({ // Estados locales const [nombre, setNombre] = useState(nombreInicial || ''); const [descripcion, setDescripcion] = useState(descripcionInicial || ''); - const [setsProductos, setSetsProductos] = useState(setsProductosInicial || []); - const [empleados, setEmpleados] = useState(empleadosInicial || []); const [mostrarAlerta, setMostrarAlerta] = useState(false); - // Estados para manejar las selecciones en las tablas - INICIALIZADOS CON LOS IDs - const [productosSeleccionados, setProductosSeleccionados] = useState( - idsSetsProductosInicial || [] - ); - const [empleadosSeleccionados, setEmpleadosSeleccionados] = useState(idsEmpleadosInicial || []); + // Estados para la lista de transferencia de empleados + const [checkedEmpleados, setCheckedEmpleados] = useState([]); + const [leftEmpleados, setLeftEmpleados] = useState([]); + const [rightEmpleados, setRightEmpleados] = useState(empleadosInicial || []); + + // Estados para la lista de transferencia de sets productos + const [checkedSets, setCheckedSets] = useState([]); + const [leftSets, setLeftSets] = useState([]); + const [rightSets, setRightSets] = useState(setsProductosInicial || []); useEffect(() => { const obtenerDatos = async () => { const productos = await obtenerSetsProductos(clienteSeleccionado); - setProductosDisponibles(productos); + console.log('Productos obtenidos:', productos); + setLeftSets(productos.filter((prod) => !rightSets.find((r) => r.id === prod.id))); const empleadosData = await obtenerEmpleados(clienteSeleccionado); - setEmpleadosDisponibles(empleadosData); + setLeftEmpleados(empleadosData.filter((emp) => !rightEmpleados.find((r) => r.id === emp.id))); }; obtenerDatos(); }, [clienteSeleccionado]); - // Efecto para sincronizar cuando cambien los IDs iniciales (por si se recarga el componente) - useEffect(() => { - if (idsSetsProductosInicial && idsSetsProductosInicial.length > 0) { - setProductosSeleccionados(idsSetsProductosInicial); - } - }, [idsSetsProductosInicial]); + // Handlers para empleados + const handleToggleEmpleados = (value) => () => { + const currentIndex = checkedEmpleados.findIndex((item) => item.id === value.id); + const newChecked = [...checkedEmpleados]; - useEffect(() => { - if (idsEmpleadosInicial && idsEmpleadosInicial.length > 0) { - setEmpleadosSeleccionados(idsEmpleadosInicial); + if (currentIndex === -1) { + newChecked.push(value); + } else { + newChecked.splice(currentIndex, 1); } - }, [idsEmpleadosInicial]); - // Efecto para mantener sincronizados los chips con las selecciones - useEffect(() => { - if (productosDisponibles.length > 0 && productosSeleccionados.length > 0) { - const productosActualizados = productosDisponibles.filter((producto) => - productosSeleccionados.includes(producto.id) - ); - setSetsProductos(productosActualizados); - } - }, [productosDisponibles, productosSeleccionados]); + setCheckedEmpleados(newChecked); + }; - useEffect(() => { - if (empleadosDisponibles.length > 0 && empleadosSeleccionados.length > 0) { - const empleadosActualizados = empleadosDisponibles.filter((empleado) => - empleadosSeleccionados.includes(empleado.id) - ); - setEmpleados(empleadosActualizados); - } - }, [empleadosDisponibles, empleadosSeleccionados]); + const handleAllLeftEmpleados = () => { + setLeftEmpleados(leftEmpleados.concat(rightEmpleados)); + setRightEmpleados([]); + }; + + const handleAllRightEmpleados = () => { + setRightEmpleados(rightEmpleados.concat(leftEmpleados)); + setLeftEmpleados([]); + }; + + const handleCheckedRightEmpleados = () => { + const leftChecked = intersection(checkedEmpleados, leftEmpleados); + setRightEmpleados(rightEmpleados.concat(leftChecked)); + setLeftEmpleados(not(leftEmpleados, leftChecked)); + setCheckedEmpleados(not(checkedEmpleados, leftChecked)); + }; + + const handleCheckedLeftEmpleados = () => { + const rightChecked = intersection(checkedEmpleados, rightEmpleados); + setLeftEmpleados(leftEmpleados.concat(rightChecked)); + setRightEmpleados(not(rightEmpleados, rightChecked)); + setCheckedEmpleados(not(checkedEmpleados, rightChecked)); + }; - // Manejar cambios en la selección de productos - const handleSeleccionProductos = (selectionData) => { - console.log('Selecciones productos recibidas:', selectionData); + // Handlers para sets productos + const handleToggleSets = (value) => () => { + const currentIndex = checkedSets.findIndex((item) => item.id === value.id); + const newChecked = [...checkedSets]; - let seleccionesArray = []; - if (selectionData && selectionData.ids && selectionData.ids instanceof Set) { - seleccionesArray = Array.from(selectionData.ids); - } else if (Array.isArray(selectionData)) { - seleccionesArray = selectionData; + if (currentIndex === -1) { + newChecked.push(value); + } else { + newChecked.splice(currentIndex, 1); } - setProductosSeleccionados(seleccionesArray); + setCheckedSets(newChecked); + }; - // Actualizar los chips basándose en las selecciones - const productosActualizados = productosDisponibles.filter((producto) => - seleccionesArray.includes(producto.id) - ); - setSetsProductos(productosActualizados); + const handleAllLeftSets = () => { + setLeftSets(leftSets.concat(rightSets)); + setRightSets([]); }; - // Manejar cambios en la selección de empleados - const handleSeleccionEmpleados = (selectionData) => { - console.log('Selecciones empleados recibidas:', selectionData); - let seleccionesArray = []; - if (selectionData && selectionData.ids && selectionData.ids instanceof Set) { - seleccionesArray = Array.from(selectionData.ids); - } else if (Array.isArray(selectionData)) { - seleccionesArray = selectionData; - } + const handleAllRightSets = () => { + setRightSets(rightSets.concat(leftSets)); + setLeftSets([]); + }; + + const handleCheckedRightSets = () => { + const leftChecked = intersection(checkedSets, leftSets); + setRightSets(rightSets.concat(leftChecked)); + setLeftSets(not(leftSets, leftChecked)); + setCheckedSets(not(checkedSets, leftChecked)); + }; - setEmpleadosSeleccionados(seleccionesArray); + const handleCheckedLeftSets = () => { + const rightChecked = intersection(checkedSets, rightSets); + setLeftSets(leftSets.concat(rightChecked)); + setRightSets(not(rightSets, rightChecked)); + setCheckedSets(not(checkedSets, rightChecked)); + }; + + const numberOfCheckedEmpleados = (items) => intersection(checkedEmpleados, items).length; + const numberOfCheckedSets = (items) => intersection(checkedSets, items).length; - // Actualizar los chips basándose en las selecciones - const empleadosActualizados = empleadosDisponibles.filter((empleado) => - seleccionesArray.includes(empleado.id) - ); - setEmpleados(empleadosActualizados); + const handleToggleAllEmpleados = (items) => () => { + if (numberOfCheckedEmpleados(items) === items.length) { + setCheckedEmpleados(not(checkedEmpleados, items)); + } else { + setCheckedEmpleados(union(checkedEmpleados, items)); + } }; - // Preparar filas para la tabla de empleados - const filas = empleadosDisponibles.map((empleado) => ({ - id: empleado.id, - nombreCompleto: empleado.nombre, - correo: empleado.correo, - areaTrabajo: empleado.area, - })); - console.log('Productos seleccionados:', productosSeleccionados); - console.log('Empleados seleccionados:', empleadosSeleccionados); + const handleToggleAllSets = (items) => () => { + if (numberOfCheckedSets(items) === items.length) { + setCheckedSets(not(checkedSets, items)); + } else { + setCheckedSets(union(checkedSets, items)); + } + }; + + const customListEmpleados = (title, items) => ( + + + } + title={title} + subheader={`${numberOfCheckedEmpleados(items)}/${items.length} seleccionados`} + /> + + + {items.map((value) => { + const labelId = `transfer-list-empleados-${value.id}-label`; + + return ( + + + item.id === value.id)} + tabIndex={-1} + disableRipple + inputProps={{ + 'aria-labelledby': labelId, + }} + /> + + + + ); + })} + + + ); + + const customListSets = (title, items) => ( + + + } + title={title} + subheader={`${numberOfCheckedSets(items)}/${items.length} seleccionados`} + /> + + + {items.map((value) => { + const labelId = `transfer-list-sets-${value.id}-label`; + + return ( + + + item.id === value.id)} + tabIndex={-1} + disableRipple + inputProps={{ + 'aria-labelledby': labelId, + }} + /> + + + + ); + })} + + + ); const handleGuardar = () => { - if (!nombre || !descripcion || setsProductos.length === 0 || empleados.length === 0) { + if (!nombre || !descripcion || rightSets.length === 0 || rightEmpleados.length === 0) { setMostrarAlerta(true); return; } console.log('Nombre:', nombre); console.log('Descripción:', descripcion); - console.log('Sets de Productos:', setsProductos); - console.log('Empleados:', empleados); - console.log('IDs Sets Productos:', productosSeleccionados); - console.log('IDs Empleados:', empleadosSeleccionados); + console.log('Sets de Productos:', rightSets); + console.log('Empleados:', rightEmpleados); }; return ( - - + + {/* Nombre */} - + Nombre: setNombre(e.target.value)} - sx={{ mt: 1 }} + sx={{ mt: 1, mb: 2, width: '300px', overflow: 'auto' }} /> {/* Descripción */} - + Descripción: setDescripcion(e.target.value)} - sx={{ mt: 1 }} + sx={{ mt: 1, mb: 2, width: '400px', overflow: 'auto' }} /> {/* Sets de Productos */} Sets de Productos: - - {setsProductos?.length > 0 ? ( - setsProductos.map((set, index) => ( - - )) - ) : ( - - No especificada - - )} + + + {customListSets('Sets Disponibles', leftSets)} + + + + + + + + + {customListSets('Sets Seleccionados', rightSets)} + - - {/* Empleados */} + {/* Lista de transferencia de empleados */} Empleados: - - {empleados?.length > 0 ? ( - empleados.map((empleado, index) => ( - - )) - ) : ( - - No especificada - - )} + + + {customListEmpleados('Empleados Disponibles', leftEmpleados)} + + + + + + + + + {customListEmpleados('Empleados Seleccionados', rightEmpleados)} + - {/* Botón Guardar */} @@ -261,7 +459,7 @@ const InfoGrupoEmpleadosEditable = ({ {mostrarAlerta && ( setMostrarAlerta(false)} diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index ea2f7bd2..4b49d0fe 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -239,7 +239,7 @@ const ListaGrupoEmpleados = () => { onClose={() => setAbrirModalEditar(false)} titulo='Editar Grupo de Empleados' tituloVariant='h4' - customWidth={700} + customWidth={900} botones={[ { label: 'Guardar', @@ -268,9 +268,9 @@ const ListaGrupoEmpleados = () => { )} From 9f2d957ee8380028ac73a50b2c1c2026eb9f592a Mon Sep 17 00:00:00 2001 From: max Date: Sun, 1 Jun 2025 17:26:45 -0600 Subject: [PATCH 047/160] Implement user update functionality with validation and API integration --- .../Modelos/Usuarios/ActualizarUsuario.js | 69 ++++ .../Usuarios/RepositorioActualizarUsuario.js | 25 ++ src/Utilidades/Constantes/rutas.js | 1 + src/Utilidades/Constantes/rutasAPI.js | 4 +- .../FormularioActualizarUsuario.jsx | 297 ++++++++++++++++++ .../Componentes/Organismos/ModalUsuarios.jsx | 5 + src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx | 117 ++++--- src/hooks/Usuarios/useAccionesUsuario.js | 139 ++++++++ src/hooks/Usuarios/useActualizarUsuario.js | 25 ++ 9 files changed, 629 insertions(+), 53 deletions(-) create mode 100644 src/Dominio/Modelos/Usuarios/ActualizarUsuario.js create mode 100644 src/Dominio/Repositorios/Usuarios/RepositorioActualizarUsuario.js create mode 100644 src/Vistas/Componentes/Organismos/Formularios/FormularioActualizarUsuario.jsx create mode 100644 src/Vistas/Componentes/Organismos/ModalUsuarios.jsx create mode 100644 src/hooks/Usuarios/useAccionesUsuario.js create mode 100644 src/hooks/Usuarios/useActualizarUsuario.js diff --git a/src/Dominio/Modelos/Usuarios/ActualizarUsuario.js b/src/Dominio/Modelos/Usuarios/ActualizarUsuario.js new file mode 100644 index 00000000..d19f142e --- /dev/null +++ b/src/Dominio/Modelos/Usuarios/ActualizarUsuario.js @@ -0,0 +1,69 @@ +// Modelo para actualizar un usuario + +export const validarDatosActualizarUsuario = (datos, usuariosExistentes = []) => { + const errores = {}; + const emailValido = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const telefonoValido = /^\d{10}$/; + + 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 && datos.contrasenia.length < 6) { + errores.contrasenia = 'La contraseña debe tener al menos 6 caracteres'; + } + + 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'; + } + + 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.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/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/Utilidades/Constantes/rutas.js b/src/Utilidades/Constantes/rutas.js index 9e391155..afd9eac0 100644 --- a/src/Utilidades/Constantes/rutas.js +++ b/src/Utilidades/Constantes/rutas.js @@ -42,6 +42,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..7d64bd3d 100644 --- a/src/Utilidades/Constantes/rutasAPI.js +++ b/src/Utilidades/Constantes/rutasAPI.js @@ -13,13 +13,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,7 +27,7 @@ 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`, diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormularioActualizarUsuario.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormularioActualizarUsuario.jsx new file mode 100644 index 00000000..f12dbb83 --- /dev/null +++ b/src/Vistas/Componentes/Organismos/Formularios/FormularioActualizarUsuario.jsx @@ -0,0 +1,297 @@ +import { useState, useEffect } 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 Alerta from '@Moleculas/Alerta'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import FormularioActualizarUsuario from '@Organismos/Formularios/FormularioActualizarUsuario'; +import { useConsultarClientes } from '@Hooks/Clientes/useConsultarClientes'; +import { useActualizarUsuario } from '@Hooks/Usuarios/useActualizarUsuario'; +import CampoSelectMultiple from '@Atomos/CampoSelectMultiple'; +import { useAccionesUsuario } from '@Hooks/Usuarios/useAccionesUsuario'; + +const FormularioActualizarUsuario = ({ open, onClose, usuario, onUsuarioActualizado }) => { + const { + datosUsuario, + setDatosUsuario, + erroresValidacion, + alerta, + setAlerta, + cargando, + esEdicion, + manejarCambio, + manejarFechaNacimiento, + obtenerHelperText, + handleGuardar, + limpiarFormulario, + CAMPO_OBLIGATORIO, + } = useAccionesUsuario(usuarioInicial); // usuarioInicial es null para crear, o el usuario para editar + + useEffect(() => { + if (usuario) { + setDatosUsuario({ + nombreCompleto: usuario.nombreCompleto || '', + apellido: usuario.apellido || '', + correoElectronico: usuario.correoElectronico || '', + numeroTelefono: usuario.numeroTelefono || '', + direccion: usuario.direccion || '', + codigoPostal: usuario.codigoPostal || '', + fechaNacimiento: usuario.fechaNacimiento || null, + genero: usuario.genero || '', + cliente: usuario.cliente || [], + rol: usuario.rol || '', + contrasenia: '', + }); + } + }, [usuario]); + + const { errores, handleActualizarUsuario } = useActualizarUsuario(); + const { roles, cargando: cargandoRoles } = useConsultarRoles(); + const { clientes } = useConsultarClientes(); + const esSuperAdmin = datosUsuario.rol === 1; + + const manejarConfirmacion = async () => { + const resultado = await handleActualizarUsuario({ + ...datosUsuario, + idUsuario: usuario.idUsuario, + }); + + if (resultado?.mensaje) { + if (resultado.exito) { + setAlerta({ + tipo: 'success', + mensaje: `Usuario actualizado exitosamente.`, + icono: true, + cerrable: true, + centradoInferior: true, + duracion: 3000, + }); + setTimeout(async () => { + if (onUsuarioActualizado) await onUsuarioActualizado(); + manejarCierre(); + }, 2000); + } else { + setAlerta({ + tipo: 'error', + mensaje: resultado.mensaje, + }); + } + } + }; + + const manejarCierre = () => { + setAlerta(null); + onClose(); + }; + + const estiloCuadricula = { + display: 'flex', + justifyContent: 'center', + }; + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + rol.idRol !== 3) + .map((rol) => ({ + value: rol.idRol, + label: rol.nombre, + }))} + disabled={cargandoRoles} + /> + + + ({ + value: cliente.idCliente, + label: cliente.nombreComercial, + }))} + disabled={esSuperAdmin} + /> + + + + + + + + {alerta && ( + setAlerta(null)} + centradoInferior + /> + )} + + ); +}; + +export default FormularioActualizarUsuario; diff --git a/src/Vistas/Componentes/Organismos/ModalUsuarios.jsx b/src/Vistas/Componentes/Organismos/ModalUsuarios.jsx new file mode 100644 index 00000000..f6a2a6ef --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ModalUsuarios.jsx @@ -0,0 +1,5 @@ +import { Box } from '@mui/material'; +import Alerta from '@Moleculas/Alerta'; +import ModalFlotante from '@Organismos/ModalFlotante'; +import FormaEmpleado from '@Organismos/Formularios/FormaEmpleado'; +import { useLeerUsuario } from '@Hooks/Usuarios/useLeerUsuario'; diff --git a/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx b/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx index 45234ef6..9711b419 100644 --- a/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx +++ b/src/Vistas/Paginas/Usuarios/ListaUsuarios.jsx @@ -21,6 +21,7 @@ import { useTheme } from '@mui/material'; import { useActivar2FA } from '@Hooks/Usuarios/useActivar2FA'; import Activar2FAModal from '@Organismos/Activar2FAModal'; import Verificar2FAModal from '@Moleculas/Verificar2FAModal'; +//import FormularioActualizarUsuario from '@Organismos/Formularios/FormularioActualizarUsuario.jsx'; const estiloImagenLogo = { marginRight: '1rem' }; @@ -44,21 +45,22 @@ const ListaUsuarios = () => { const navigate = useNavigate(); const [alerta, setAlerta] = useState(null); const { usuarios, cargando, error, recargar } = useConsultarListaUsuarios(); - const { roles} = useConsultarRoles(); + 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 manejarActivar2FA = async () => { + /** const manejarActivar2FA = async () => { await activar2FA({ idUsuario: usuarioAutenticado?.idUsuario, nombre: usuarioAutenticado?.nombre, @@ -68,7 +70,7 @@ const ListaUsuarios = () => { }; */ const opcionesRol = roles.map((rol) => ({ - value: rol.idRol, + value: rol.idRol, label: rol.nombre, })); @@ -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]); @@ -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 }); @@ -388,8 +391,11 @@ const { variant: 'contained', color: 'primary', backgroundColor: colores.altertex[1], - onClick: () => console.log('Editar usuario'), - disabled: true, + onClick: () => { + setModalDetalleAbierto(false); + setTimeout(() => setModalActualizarAbierto(true), 100); + }, + disabled: !usuarioAutenticado?.permisos?.includes(PERMISOS.ACTUALIZAR_USUARIO), }, { label: 'SALIR', @@ -404,34 +410,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={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} + /> ) : (

No se encontró información del usuario.

@@ -439,6 +445,15 @@ const { )} + {modalActualizarAbierto && usuario && ( + setModalActualizarAbierto(false)} + onAccion={recargar} + usuarioEdicion={usuario} + /> + )} + {errorDetalle && (
diff --git a/src/hooks/Usuarios/useAccionesUsuario.js b/src/hooks/Usuarios/useAccionesUsuario.js new file mode 100644 index 00000000..08284b03 --- /dev/null +++ b/src/hooks/Usuarios/useAccionesUsuario.js @@ -0,0 +1,139 @@ +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); + + // Inicializar con datos de edición o vacío + const [datosUsuario, setDatosUsuario] = useState(() => { + if (esEdicion) { + let fechaNacimiento = null; + if (usuarioInicial.fechaNacimiento) { + fechaNacimiento = dayjs(usuarioInicial.fechaNacimiento); + } + return { + ...usuarioInicial, + idUsuario: usuarioInicial.idUsuario || usuarioInicial.id, + nombreCompleto: usuarioInicial.nombreCompleto || '', + correoElectronico: usuarioInicial.correoElectronico || '', + fechaNacimiento, + }; + } + return { + nombreCompleto: '', + correoElectronico: '', + contrasenia: '', + numeroTelefono: '', + direccion: '', + fechaNacimiento: null, + genero: '', + rol: '', + cliente: [], + }; + }); + + 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, + }; + + const nuevosErrores = validarDatosActualizarUsuario(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 RepositorioActualizarUsuario.actualizar(datosProcesados); + setAlerta({ + tipo: 'success', + mensaje: esEdicion ? 'Usuario actualizado correctamente' : 'Usuario creado correctamente', + }); + return { + exito: true, + mensaje: esEdicion ? 'Usuario actualizado correctamente' : 'Usuario creado 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: '', + rol: '', + 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 }; +} From f06e33a511726074b55435175b55d54cbbcb5e7e Mon Sep 17 00:00:00 2001 From: Diego Alfaro Pinto Date: Sun, 1 Jun 2025 17:30:36 -0600 Subject: [PATCH 048/160] feat: agregar componente de lista de transferencia --- .../Organismos/ListaTransferencia.jsx | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 src/Vistas/Componentes/Organismos/ListaTransferencia.jsx diff --git a/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx new file mode 100644 index 00000000..6871ee7e --- /dev/null +++ b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx @@ -0,0 +1,200 @@ +import React, { useState, useEffect } 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'; + +// Funciones utilitarias +function no(a, b, funcionClave = (elemento) => elemento.id || elemento) { + return a.filter((valorA) => !b.some((valorB) => funcionClave(valorA) === funcionClave(valorB))); +} + +function interseccion(a, b, funcionClave = (elemento) => elemento.id || elemento) { + return a.filter((valorA) => b.some((valorB) => funcionClave(valorA) === funcionClave(valorB))); +} + +function union(a, b, funcionClave = (elemento) => elemento.id || elemento) { + return [...a, ...no(b, a, 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 = 230, + ancho = 250 + }) => { + const [marcados, setMarcados] = useState([]); + const [izquierda, setIzquierda] = useState(elementosDisponibles); + const [derecha, setDerecha] = useState(elementosSeleccionados); + + // Actualizar estado interno cuando cambien las props + useEffect(() => { + setIzquierda(elementosDisponibles); + }, [elementosDisponibles]); + + useEffect(() => { + setDerecha(elementosSeleccionados); + }, [elementosSeleccionados]); + + // Notificar al componente padre de los cambios + useEffect(() => { + if (alCambiarSeleccion) { + 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 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 From 0f1d75e44a22f7237a2ca7ae867d450a8261d6eb Mon Sep 17 00:00:00 2001 From: Diego Alfaro Pinto Date: Sun, 1 Jun 2025 17:32:01 -0600 Subject: [PATCH 049/160] fix: cambiar nombre de el componente --- src/Vistas/Componentes/Organismos/ListaTransferencia.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx index 6871ee7e..f0bb18d4 100644 --- a/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx +++ b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx @@ -24,7 +24,7 @@ function union(a, b, funcionClave = (elemento) => elemento.id || elemento) { } // Componente Lista de Transferencia Personalizada -const ListaTransferenciaPersonalizada = ({ +const ListaTransferencia = ({ elementosDisponibles = [], elementosSeleccionados = [], alCambiarSeleccion, @@ -197,4 +197,4 @@ const ListaTransferenciaPersonalizada = ({ ); }; -export default ListaTransferenciaPersonalizada; \ No newline at end of file +export default ListaTransferencia; \ No newline at end of file From 17eb133995da902efb63386ef2d647f1d2f939fb Mon Sep 17 00:00:00 2001 From: angieriosc Date: Sun, 1 Jun 2025 17:32:31 -0600 Subject: [PATCH 050/160] Fix: recibir sets de productos para actualizar --- .../Paginas/Empleados/ListaGrupoEmpleados.jsx | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index 4b49d0fe..0c422f54 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -1,6 +1,6 @@ // 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 React, { use, useState } from 'react'; import Tabla from '@Organismos/Tabla'; import ContenedorLista from '@Organismos/ContenedorLista'; import { useConsultarGrupos } from '@Hooks/Empleados/useConsultarGrupos'; @@ -16,6 +16,7 @@ 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(); @@ -33,6 +34,7 @@ const ListaGrupoEmpleados = () => { const [modalDetalleAbierto, setModalDetalleAbierto] = useState(false); const [idGrupoSeleccionado, setIdGrupoSeleccionado] = useState(null); const [abrirModalEditar, setAbrirModalEditar] = useState(false); + const [formData, setFormData] = useState(null); const { grupoEmpleados, cargando: cargandoDetalle, @@ -66,6 +68,50 @@ const ListaGrupoEmpleados = () => { } }; + const handleFormDataChange = (data) => { + setFormData(data); + }; + + const handleGuardar = async () => { + if (!formData?.isValid) { + setAlerta({ + tipo: 'warning', + mensaje: 'Por favor completa todos los campos requeridos', + icono: true, + cerrable: true, + centradoInferior: true, + }); + return; + } + console.log('Guardando grupo de empleados:', formData); + try { + useActualizarGrupoEmpleados( + idGrupoSeleccionado, + formData.nombre, + formData.descripcion, + formData.empleados, + formData.setsDeProductos + ); + setAbrirModalEditar(false); + await refetch(); + setAlerta({ + tipo: 'success', + mensaje: 'Grupo de empleados actualizado correctamente.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + } catch (error) { + setAlerta({ + tipo: 'error', + mensaje: 'Error al actualizar el grupo de empleados.', + icono: true, + cerrable: true, + centradoInferior: true, + }); + } + }; + const columnas = [ { field: 'nombre', @@ -246,10 +292,8 @@ const ListaGrupoEmpleados = () => { variant: 'contained', color: 'primary', outlineColor: colores.primario[10], - onClick: () => { - setAbrirModalEditar(false); - refetch(); - }, + onClick: handleGuardar, + disabled: !formData?.isValid, }, { label: 'Cancelar', @@ -272,6 +316,7 @@ const ListaGrupoEmpleados = () => { idsSetProductos={grupoEmpleados?.idsSetProductos || []} empleados={grupoEmpleados?.empleadosActualizar || []} idsEmpleados={grupoEmpleados?.idsEmpleados || []} + onFormDataChange={handleFormDataChange} /> )} From 661a22e903b457529347303923f156023cc4198f Mon Sep 17 00:00:00 2001 From: Diego Alfaro Pinto Date: Sun, 1 Jun 2025 18:01:24 -0600 Subject: [PATCH 051/160] feat: render de lista de transferencia --- .../Organismos/ModalDetalleRol.jsx | 139 +++++++++++++++--- 1 file changed, 117 insertions(+), 22 deletions(-) diff --git a/src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx b/src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx index d4652f68..49174820 100644 --- a/src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx +++ b/src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx @@ -1,22 +1,57 @@ // RF[8] Leer Rol - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF8 -import { useEffect } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { Box, Typography, useTheme } from '@mui/material'; import ModalFlotante from '@Organismos/ModalFlotante'; import { useLeerRol } from '@Hooks/Roles/useLeerRol'; import Alerta from '@Moleculas/Alerta'; import Tabla from '@Organismos/Tabla'; +import ListaTransferencia from '@Organismos/ListaTransferencia'; +import obtenerPermisos from '@Servicios/obtenerPermisos'; import { tokens } from '@SRC/theme'; const ModalDetalleRol = ({ abierto, onCerrar, idRol }) => { const { detalle, cargando, error, leerRol } = useLeerRol(); const theme = useTheme(); const colores = tokens(theme.palette.mode); + const [modoEdicion, setModoEdicion] = useState(false); + const [permisosDisponibles, setPermisosDisponibles] = useState([]); + const [permisosSeleccionados, setPermisosSeleccionados] = useState([]); + + // Add ref to track if permissions have been loaded + const permisosLoadedRef = useRef(false); useEffect(() => { if (abierto && idRol) leerRol(idRol); }, [abierto, idRol, leerRol]); + // Reset the ref when modal closes or role changes + useEffect(() => { + if (!abierto || !modoEdicion) { + permisosLoadedRef.current = false; + } + }, [abierto, modoEdicion]); + + // Modified useEffect with ref check to prevent infinite loop + 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, permisosLoadedRef]); + const columnas = [ { field: 'nombre', @@ -36,6 +71,55 @@ const ModalDetalleRol = ({ abierto, onCerrar, idRol }) => { descripcion: permiso.descripcion, })); + const manejarCambioEdicion = () => { + setModoEdicion(!modoEdicion); + }; + + const manejarCambioTransferencia = ({ disponibles, seleccionados }) => { + setPermisosDisponibles(disponibles); + setPermisosSeleccionados(seleccionados); + }; + + const manejarCerrar = () => { + setModoEdicion(false); + onCerrar(); + }; + + const botonesModo = modoEdicion ? [ + { + label: 'Cancelar', + variant: 'outlined', + size: 'large', + outlineColor: colores.altertex[1], + onClick: () => setModoEdicion(false), + }, + { + label: 'Guardar', + variant: 'contained', + size: 'large', + backgroundColor: colores.altertex[1], + onClick: () => { + // Aquí iría la lógica para guardar + setModoEdicion(false); + }, + } + ] : [ + { + 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 && ( @@ -62,20 +146,12 @@ const ModalDetalleRol = ({ abierto, onCerrar, idRol }) => { {cargando && ( @@ -95,14 +171,33 @@ const ModalDetalleRol = ({ abierto, onCerrar, idRol }) => { Número de usuarios asociados a este rol: {detalle.totalUsuarios} - - - + {modoEdicion ? ( + + + Gestionar Permisos + + permiso.nombre} + obtenerClaveElemento={(permiso) => permiso.id} + alturaMaxima={300} + ancho={300} + /> + + ) : ( + + + + )} )} From 4961f7f8a019fb3c857054fc011fbd67ebfa538a Mon Sep 17 00:00:00 2001 From: Diego Alfaro Pinto Date: Sun, 1 Jun 2025 18:02:14 -0600 Subject: [PATCH 052/160] fix: arreglar loop infinito --- .../Organismos/ListaTransferencia.jsx | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx index f0bb18d4..1c5710ad 100644 --- a/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx +++ b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +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'; @@ -25,33 +25,40 @@ function union(a, b, funcionClave = (elemento) => elemento.id || elemento) { // Componente Lista de Transferencia Personalizada const ListaTransferencia = ({ - 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 = 230, - ancho = 250 - }) => { + 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 = 230, + ancho = 250 + }) => { const [marcados, setMarcados] = useState([]); const [izquierda, setIzquierda] = useState(elementosDisponibles); const [derecha, setDerecha] = useState(elementosSeleccionados); + // Use ref to track if we're in the middle of updating from props + 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]); - // Notificar al componente padre de los cambios + // Only notify parent when changes come from user interactions, not from prop updates useEffect(() => { - if (alCambiarSeleccion) { + if (alCambiarSeleccion && !updatingFromPropsRef.current) { alCambiarSeleccion({ disponibles: izquierda, seleccionados: derecha From cdb1cf796ca736f1d88c73d5f377dabc8a533f7b Mon Sep 17 00:00:00 2001 From: Diego Alfaro Pinto Date: Sun, 1 Jun 2025 18:03:55 -0600 Subject: [PATCH 053/160] fix: arreglar loop infinito en la lista de transferencia --- src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx b/src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx index 49174820..9a926135 100644 --- a/src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx +++ b/src/Vistas/Componentes/Organismos/ModalDetalleRol.jsx @@ -1,6 +1,6 @@ // RF[8] Leer Rol - https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF8 -import { useEffect, useState, useRef } from 'react'; +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'; @@ -50,7 +50,7 @@ const ModalDetalleRol = ({ abierto, onCerrar, idRol }) => { }; cargarPermisos(); - }, [detalle, modoEdicion, permisosLoadedRef]); + }, [detalle, modoEdicion]); const columnas = [ { @@ -75,10 +75,10 @@ const ModalDetalleRol = ({ abierto, onCerrar, idRol }) => { setModoEdicion(!modoEdicion); }; - const manejarCambioTransferencia = ({ disponibles, seleccionados }) => { + const manejarCambioTransferencia = useCallback(({ disponibles, seleccionados }) => { setPermisosDisponibles(disponibles); setPermisosSeleccionados(seleccionados); - }; + }, []); const manejarCerrar = () => { setModoEdicion(false); From 06706dac579d66e14ef6d5cb4e4752c559aad222 Mon Sep 17 00:00:00 2001 From: Diego Alfaro Pinto Date: Sun, 1 Jun 2025 18:07:28 -0600 Subject: [PATCH 054/160] feat: agregar funcionalidad de seleccionar y quitar todo con flechas --- .../Organismos/ListaTransferencia.jsx | 58 ++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx index 1c5710ad..aacd3dc4 100644 --- a/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx +++ b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx @@ -24,18 +24,18 @@ function union(a, b, funcionClave = (elemento) => elemento.id || elemento) { } // Componente Lista de Transferencia Personalizada -const ListaTransferencia = ({ - 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 = 230, - ancho = 250 - }) => { +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 = 230, + ancho = 250 + }) => { const [marcados, setMarcados] = useState([]); const [izquierda, setIzquierda] = useState(elementosDisponibles); const [derecha, setDerecha] = useState(elementosSeleccionados); @@ -104,6 +104,18 @@ const ListaTransferencia = ({ 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) => ( {listaPersonalizada(tituloIzquierda, izquierda)} + + {listaPersonalizada(tituloDerecha, derecha)} @@ -204,4 +236,4 @@ const ListaTransferencia = ({ ); }; -export default ListaTransferencia; \ No newline at end of file +export default ListaTransferenciaPersonalizada; \ No newline at end of file From 25d8b10f556f1323bf4540620f95ef4a5d7ff96f Mon Sep 17 00:00:00 2001 From: Diego Alfaro Pinto Date: Sun, 1 Jun 2025 18:15:05 -0600 Subject: [PATCH 055/160] feat: cambiar flechas a iconos de MUI --- .../Componentes/Organismos/ListaTransferencia.jsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx index aacd3dc4..75901460 100644 --- a/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx +++ b/src/Vistas/Componentes/Organismos/ListaTransferencia.jsx @@ -9,6 +9,11 @@ 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(a, b, funcionClave = (elemento) => elemento.id || elemento) { @@ -197,7 +202,7 @@ const ListaTransferenciaPersonalizada = ({ disabled={izquierda.length === 0 || deshabilitado} aria-label="mover todo a la derecha" > - ≫ + From 846924b30ae070d0b1a8fff6df0f6f549366a87b Mon Sep 17 00:00:00 2001 From: angieriosc Date: Sun, 1 Jun 2025 20:24:24 -0600 Subject: [PATCH 056/160] =?UTF-8?q?Fix:=20optimizar=20c=C3=B3digo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Moleculas/GrupoEmpleadosInfoEditable.jsx | 67 ++++++++++------ .../Paginas/Empleados/ListaGrupoEmpleados.jsx | 11 ++- .../Empleados/useActualizarGrupoEmpleados.js | 77 ++++++++----------- 3 files changed, 86 insertions(+), 69 deletions(-) diff --git a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx index 2fa55790..ca7b3afb 100644 --- a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx +++ b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx @@ -1,6 +1,6 @@ import Alerta from '@Moleculas/Alerta'; import CampoTexto from '@Atomos/CampoTexto'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import obtenerSetsProductos from '@Servicios/obtenerSetsProductos'; import obtenerEmpleados from '@Servicios/obtenerEmpleados'; import { useAuth } from '@Hooks/AuthProvider'; @@ -37,9 +37,8 @@ const InfoGrupoEmpleadosEditable = ({ descripcion: descripcionInicial, setsProductos: setsProductosInicial, empleados: empleadosInicial, + onFormDataChange, }) => { - const [productosDisponibles, setProductosDisponibles] = useState([]); - const [empleadosDisponibles, setEmpleadosDisponibles] = useState([]); const { usuario } = useAuth(); const clienteSeleccionado = usuario.clienteSeleccionado; @@ -58,10 +57,27 @@ const InfoGrupoEmpleadosEditable = ({ const [leftSets, setLeftSets] = useState([]); const [rightSets, setRightSets] = useState(setsProductosInicial || []); + const rightSetsIds = useMemo( + () => + rightSets + .map((item) => item.id) + .sort() + .join(','), + [rightSets] + ); + + const rightEmpleadosIds = useMemo( + () => + rightEmpleados + .map((item) => item.id) + .sort() + .join(','), + [rightEmpleados] + ); + useEffect(() => { const obtenerDatos = async () => { const productos = await obtenerSetsProductos(clienteSeleccionado); - console.log('Productos obtenidos:', productos); setLeftSets(productos.filter((prod) => !rightSets.find((r) => r.id === prod.id))); const empleadosData = await obtenerEmpleados(clienteSeleccionado); @@ -71,6 +87,29 @@ const InfoGrupoEmpleadosEditable = ({ obtenerDatos(); }, [clienteSeleccionado]); + useEffect(() => { + const isValid = Boolean( + nombre && descripcion && rightSets.length > 0 && rightEmpleados.length > 0 + ); + + if (onFormDataChange) { + onFormDataChange({ + isValid, + nombre, + descripcion, + setsDeProductos: rightSets.map((set) => set.id), + empleados: rightEmpleados.map((emp) => emp.id), + }); + } + }, [ + nombre, + descripcion, + rightSets.length, + rightEmpleados.length, + rightSetsIds, + rightEmpleadosIds, + ]); + // Handlers para empleados const handleToggleEmpleados = (value) => () => { const currentIndex = checkedEmpleados.findIndex((item) => item.id === value.id); @@ -285,18 +324,6 @@ const InfoGrupoEmpleadosEditable = ({ ); - const handleGuardar = () => { - if (!nombre || !descripcion || rightSets.length === 0 || rightEmpleados.length === 0) { - setMostrarAlerta(true); - return; - } - - console.log('Nombre:', nombre); - console.log('Descripción:', descripcion); - console.log('Sets de Productos:', rightSets); - console.log('Empleados:', rightEmpleados); - }; - return ( @@ -447,13 +474,6 @@ const InfoGrupoEmpleadosEditable = ({ - - {/* Botón Guardar */} - - - {mostrarAlerta && ( @@ -466,6 +486,7 @@ const InfoGrupoEmpleadosEditable = ({ sx={{ mb: 2, mt: 2 }} /> )} + ); }; diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index 5b781858..917755bc 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -72,6 +72,8 @@ const ListaGrupoEmpleados = () => { setFormData(data); }; + const { actualizarGrupo } = useActualizarGrupoEmpleados(); + const handleGuardar = async () => { if (!formData?.isValid) { setAlerta({ @@ -83,15 +85,20 @@ const ListaGrupoEmpleados = () => { }); return; } - console.log('Guardando grupo de empleados:', formData); try { - useActualizarGrupoEmpleados( + console.log('Datos a enviar:', { + idGrupo: idGrupoSeleccionado, + ...formData, + }); + + await actualizarGrupo( idGrupoSeleccionado, formData.nombre, formData.descripcion, formData.empleados, formData.setsDeProductos ); + setAbrirModalEditar(false); await refetch(); setAlerta({ diff --git a/src/hooks/Empleados/useActualizarGrupoEmpleados.js b/src/hooks/Empleados/useActualizarGrupoEmpleados.js index f67b327e..f3a764e8 100644 --- a/src/hooks/Empleados/useActualizarGrupoEmpleados.js +++ b/src/hooks/Empleados/useActualizarGrupoEmpleados.js @@ -1,58 +1,47 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { RepositorioActualizarGrupoEmpleados } from '@Repositorios/Empleados/RepositorioActualizarGrupoEmpleados'; /** - * * Hook para actualizar los datos de un grupo de empleados - * @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 {{ - * mensaje: string, - * * cargando: boolean, - * * error: string | null - * * }} - * * @see [RF[24] Actualizar grupo de empleados - [https://codeandco-wiki.netlify.app/docs/proyectos/textiles/documentacion/requisitos/RF24) + * 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 = ( - idGrupo, - nombre, - descripcion, - empleados, - setsDeProductos -) => { +export const useActualizarGrupoEmpleados = () => { const [mensaje, setMensaje] = useState(''); const [cargando, setCargando] = useState(false); const [error, setError] = useState(null); - useEffect(() => { - const actualizarGrupoEmpleados = async () => { - setCargando(true); - setError(null); + const actualizarGrupo = async (idGrupo, nombre, descripcion, empleados, setsDeProductos) => { + if (!idGrupo) return; + + setCargando(true); + setError(null); - try { - const { mensaje } = await RepositorioActualizarGrupoEmpleados.actualizarGrupoEmpleados( - idGrupo, - nombre, - descripcion, - empleados, - setsDeProductos - ); + try { + console.log('useActualizarGrupoEmpleados:', { + idGrupo, + nombre, + descripcion, + empleados, + setsDeProductos, + }); - setMensaje(mensaje); - } catch (err) { - setError(err.message); - } finally { - setCargando(false); - } - }; + const { mensaje } = await RepositorioActualizarGrupoEmpleados.actualizarGrupoEmpleados( + idGrupo, + nombre, + descripcion, + empleados, + setsDeProductos + ); - if (idGrupo) { - actualizarGrupoEmpleados(); + setMensaje(mensaje); + return mensaje; + } catch (err) { + setError(err.message); + throw err; + } finally { + setCargando(false); } - }, [idGrupo, nombre, descripcion, empleados, setsDeProductos]); + }; - return { mensaje, cargando, error }; + return { actualizarGrupo, mensaje, cargando, error }; }; From 31edc70802a6a5fcf9bbce49e9b8aabda2717736 Mon Sep 17 00:00:00 2001 From: angieriosc Date: Sun, 1 Jun 2025 20:25:36 -0600 Subject: [PATCH 057/160] Fix: quitar console.log --- src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx | 5 ----- src/hooks/Empleados/useActualizarGrupoEmpleados.js | 8 -------- 2 files changed, 13 deletions(-) diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index 917755bc..4166de60 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -86,11 +86,6 @@ const ListaGrupoEmpleados = () => { return; } try { - console.log('Datos a enviar:', { - idGrupo: idGrupoSeleccionado, - ...formData, - }); - await actualizarGrupo( idGrupoSeleccionado, formData.nombre, diff --git a/src/hooks/Empleados/useActualizarGrupoEmpleados.js b/src/hooks/Empleados/useActualizarGrupoEmpleados.js index f3a764e8..0a6f7e8a 100644 --- a/src/hooks/Empleados/useActualizarGrupoEmpleados.js +++ b/src/hooks/Empleados/useActualizarGrupoEmpleados.js @@ -17,14 +17,6 @@ export const useActualizarGrupoEmpleados = () => { setError(null); try { - console.log('useActualizarGrupoEmpleados:', { - idGrupo, - nombre, - descripcion, - empleados, - setsDeProductos, - }); - const { mensaje } = await RepositorioActualizarGrupoEmpleados.actualizarGrupoEmpleados( idGrupo, nombre, From 80254e72ac99a778ed63cc0eb9b957b09accbc46 Mon Sep 17 00:00:00 2001 From: Diego Alfaro Pinto Date: Mon, 2 Jun 2025 11:30:03 -0600 Subject: [PATCH 058/160] fix: arreglar errores de lint --- src/Dominio/Modelos/Productos/InfoProducto.js | 4 ++-- .../Organismos/Formularios/FormaCrearSetsProducto.jsx | 2 -- src/Vistas/Paginas/Categorias/ListaCategorias.jsx | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Dominio/Modelos/Productos/InfoProducto.js b/src/Dominio/Modelos/Productos/InfoProducto.js index b4ff8db1..d9ef51e9 100644 --- a/src/Dominio/Modelos/Productos/InfoProducto.js +++ b/src/Dominio/Modelos/Productos/InfoProducto.js @@ -24,7 +24,7 @@ export class InfoProducto { // Variantes del producto (puede ser un arreglo vacío) this.variantes = Array.isArray(producto.variantes) - ? producto.variantes.map((v) => new VarianteProducto(v)) + ? producto.variantes.map((variante) => new VarianteProducto(variante)) : []; } } @@ -35,7 +35,7 @@ class VarianteProducto { this.nombreVariante = nombreVariante ?? ''; this.descripcion = descripcion ?? ''; this.opciones = Array.isArray(opciones) - ? opciones.map((o) => new OpcionVariante(o)) + ? opciones.map((opcion) => new OpcionVariante(opcion)) : []; } } diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearSetsProducto.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearSetsProducto.jsx index 1d9ca7a0..d5f4a706 100644 --- a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearSetsProducto.jsx +++ b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearSetsProducto.jsx @@ -25,8 +25,6 @@ const FormaCrearSetsProducto = ({ setDescripcionSetsProducto, productos, setProductos, - mostrarAlerta, - setMostrarAlerta, erroresCampos, setErroresCampos, }) => { diff --git a/src/Vistas/Paginas/Categorias/ListaCategorias.jsx b/src/Vistas/Paginas/Categorias/ListaCategorias.jsx index 8fd83042..46aedc93 100644 --- a/src/Vistas/Paginas/Categorias/ListaCategorias.jsx +++ b/src/Vistas/Paginas/Categorias/ListaCategorias.jsx @@ -67,7 +67,7 @@ const ListaCategorias = () => { try { const detalle = await leerCategoria(idCategoria); setCategoriaDetalle(detalle); - } catch (err) { + } catch { setErrorDetalle(true); setCategoriaDetalle({ nombreCategoria: '', From 37c270a3487654f85b936178b9bba1b2ffdfd398 Mon Sep 17 00:00:00 2001 From: angieriosc Date: Mon, 2 Jun 2025 11:52:15 -0600 Subject: [PATCH 059/160] Fix: empleados y sets de productos pueden estar vacios al actualizar --- .../Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx index ca7b3afb..85ceb687 100644 --- a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx +++ b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx @@ -88,9 +88,7 @@ const InfoGrupoEmpleadosEditable = ({ }, [clienteSeleccionado]); useEffect(() => { - const isValid = Boolean( - nombre && descripcion && rightSets.length > 0 && rightEmpleados.length > 0 - ); + const isValid = Boolean(nombre && descripcion); if (onFormDataChange) { onFormDataChange({ @@ -264,6 +262,9 @@ const InfoGrupoEmpleadosEditable = ({ ); + const LIMITE_NOMBRE = 50; + const LIMITE_DESCRIPCION = 150; + const MENSAJE_LIMITE = 'Máximo caracteres'; const customListSets = (title, items) => ( From 39ce63261b1b39f8b5dbd455ba3493e16aaa2dab Mon Sep 17 00:00:00 2001 From: angieriosc Date: Mon, 2 Jun 2025 12:21:01 -0600 Subject: [PATCH 060/160] =?UTF-8?q?Feat:=20Nombre=20y=20descripci=C3=B3n?= =?UTF-8?q?=20son=20campos=20obligatorios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Moleculas/GrupoEmpleadosInfoEditable.jsx | 60 ++++++++++++------- .../Paginas/Empleados/ListaGrupoEmpleados.jsx | 2 +- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx index 85ceb687..26f3a064 100644 --- a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx +++ b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx @@ -19,6 +19,11 @@ import { } from '@mui/material'; import Texto from '@Atomos/Texto'; +// Constantes para los límites de caracteres +const LIMITE_NOMBRE = 50; +const LIMITE_DESCRIPCION = 150; +const MENSAJE_LIMITE = 'Máximo caracteres'; + // Funciones auxiliares para la lista de transferencia function not(a, b) { return a.filter((value) => !b.find((item) => item.id === value.id)); @@ -45,6 +50,7 @@ const InfoGrupoEmpleadosEditable = ({ // Estados locales const [nombre, setNombre] = useState(nombreInicial || ''); const [descripcion, setDescripcion] = useState(descripcionInicial || ''); + const [errores, setErrores] = useState({}); const [mostrarAlerta, setMostrarAlerta] = useState(false); // Estados para la lista de transferencia de empleados @@ -87,8 +93,29 @@ const InfoGrupoEmpleadosEditable = ({ obtenerDatos(); }, [clienteSeleccionado]); + // Validación de campos + const validarCampos = () => { + const nuevosErrores = {}; + + if (!nombre.trim()) { + nuevosErrores.nombre = 'Este campo es obligatorio'; + } else if (nombre.length > LIMITE_NOMBRE) { + nuevosErrores.nombre = `El nombre no puede exceder ${LIMITE_NOMBRE} caracteres`; + } + + if (!descripcion.trim()) { + nuevosErrores.descripcion = 'Este campo es obligatorio'; + } else if (descripcion.length > LIMITE_DESCRIPCION) { + nuevosErrores.descripcion = `La descripción no puede exceder ${LIMITE_DESCRIPCION} caracteres`; + } + + setErrores(nuevosErrores); + return Object.keys(nuevosErrores).length === 0; + }; + + // Efecto para validar y notificar cambios useEffect(() => { - const isValid = Boolean(nombre && descripcion); + const isValid = validarCampos(); if (onFormDataChange) { onFormDataChange({ @@ -99,14 +126,7 @@ const InfoGrupoEmpleadosEditable = ({ empleados: rightEmpleados.map((emp) => emp.id), }); } - }, [ - nombre, - descripcion, - rightSets.length, - rightEmpleados.length, - rightSetsIds, - rightEmpleadosIds, - ]); + }, [nombre, descripcion, rightSets, rightEmpleados]); // Handlers para empleados const handleToggleEmpleados = (value) => () => { @@ -262,10 +282,6 @@ const InfoGrupoEmpleadosEditable = ({ ); - const LIMITE_NOMBRE = 50; - const LIMITE_DESCRIPCION = 150; - const MENSAJE_LIMITE = 'Máximo caracteres'; - const customListSets = (title, items) => ( setNombre(e.target.value)} + error={!!errores.nombre} + helperText={errores.nombre || `${nombre.length}/${LIMITE_NOMBRE} ${MENSAJE_LIMITE}`} + inputProps={{ maxLength: LIMITE_NOMBRE }} sx={{ mt: 1, mb: 2, width: '300px', overflow: 'auto' }} /> @@ -350,6 +369,11 @@ const InfoGrupoEmpleadosEditable = ({ value={descripcion} placeholder='Escribe una descripción' onChange={(e) => setDescripcion(e.target.value)} + error={!!errores.descripcion} + helperText={ + errores.descripcion || `${descripcion.length}/${LIMITE_DESCRIPCION} ${MENSAJE_LIMITE}` + } + inputProps={{ maxLength: LIMITE_DESCRIPCION }} sx={{ mt: 1, mb: 2, width: '400px', overflow: 'auto' }} /> @@ -477,16 +501,6 @@ const InfoGrupoEmpleadosEditable = ({ - {mostrarAlerta && ( - setMostrarAlerta(false)} - sx={{ mb: 2, mt: 2 }} - /> - )} ); diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index 4166de60..612d563c 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -178,7 +178,7 @@ const ListaGrupoEmpleados = () => { ]; const manejarGrupoCreadoExitosamente = () => { - refetch(); // Recarga la lista de grupos + refetch(); setAlerta({ tipo: 'success', mensaje: 'Grupo de empleados creado correctamente.', From 5193269a13b90049651ac47c75d706c6ec4ad02f Mon Sep 17 00:00:00 2001 From: angieriosc Date: Mon, 2 Jun 2025 13:27:10 -0600 Subject: [PATCH 061/160] Feat: Manejo de errores --- .../RepositorioActualizarGrupoEmpleados.js | 20 ++++----- .../Moleculas/GrupoEmpleadosInfoEditable.jsx | 2 +- .../Formularios/FormaCrearCategoria.jsx | 37 ++++++++-------- .../Paginas/Empleados/ListaGrupoEmpleados.jsx | 12 +---- .../Empleados/useActualizarGrupoEmpleados.js | 44 +++++++++++++++---- 5 files changed, 67 insertions(+), 48 deletions(-) diff --git a/src/Dominio/Repositorios/Empleados/RepositorioActualizarGrupoEmpleados.js b/src/Dominio/Repositorios/Empleados/RepositorioActualizarGrupoEmpleados.js index 72de1e5b..1f9c52e7 100644 --- a/src/Dominio/Repositorios/Empleados/RepositorioActualizarGrupoEmpleados.js +++ b/src/Dominio/Repositorios/Empleados/RepositorioActualizarGrupoEmpleados.js @@ -18,13 +18,13 @@ export class RepositorioActualizarGrupoEmpleados { static async actualizarGrupoEmpleados(idGrupo, nombre, descripcion, empleados, setsDeProductos) { try { const respuesta = await axios.put( - RUTAS_API.EMPLEADOS.ACTUALIZAR_GRUPO, + `${RUTAS_API.EMPLEADOS.ACTUALIZAR_GRUPO}`, { idGrupoEmpleado: idGrupo, - nombre: nombre, - descripcion: descripcion, - empleados: empleados, - setsDeProductos: setsDeProductos, + nombre, + descripcion, + empleados, + setsDeProductos, }, { headers: { @@ -34,12 +34,12 @@ export class RepositorioActualizarGrupoEmpleados { } ); - const { mensaje } = respuesta.data; - - return { mensaje }; + return respuesta; } catch (error) { - const mensaje = error.response?.data?.mensaje || 'Error al actualizar el grupo de empleados.'; - throw new Error(mensaje); + if (error.response?.data?.mensaje) { + throw new Error(error.response.data.mensaje); + } + throw error; } } } diff --git a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx index 26f3a064..cab951af 100644 --- a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx +++ b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx @@ -356,7 +356,7 @@ const InfoGrupoEmpleadosEditable = ({ error={!!errores.nombre} helperText={errores.nombre || `${nombre.length}/${LIMITE_NOMBRE} ${MENSAJE_LIMITE}`} inputProps={{ maxLength: LIMITE_NOMBRE }} - sx={{ mt: 1, mb: 2, width: '300px', overflow: 'auto' }} + sx={{ mt: 1, mb: 2, mr: 8, width: '300px', overflow: 'auto' }} /> diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx index 918b19f5..9a8e4dc0 100644 --- a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx +++ b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx @@ -1,4 +1,3 @@ -import Alerta from '@Moleculas/Alerta'; import CampoTexto from '@Atomos/CampoTexto'; import { useState, useEffect } from 'react'; import obtenerProductos from '@Servicios/obtenerProductos'; @@ -20,17 +19,17 @@ const LIMITE_DESCRIPCION = 150; const MENSAJE_LIMITE = 'Máximo caracteres'; const FormaCrearCategorias = ({ - nombreCategoria, - setNombreCategoria, - descripcionCategoria, - setDescripcionCategoria, - productos, - setProductos, - mostrarAlerta, - setMostrarAlerta, - errores, - intentoEnviar, - }) => { + nombreCategoria, + setNombreCategoria, + descripcionCategoria, + setDescripcionCategoria, + productos, + setProductos, + mostrarAlerta, + setMostrarAlerta, + errores, + intentoEnviar, +}) => { const [rows, setRows] = useState([]); const { usuario } = useAuth(); const clienteSeleccionado = usuario.clienteSeleccionado; @@ -80,7 +79,8 @@ const FormaCrearCategorias = ({ onChange={(evento) => setNombreCategoria(evento.target.value.slice(0, LIMITE_NOMBRE))} inputProps={{ maxLength: LIMITE_NOMBRE }} helperText={ - errores?.nombreCategoria || `${nombreCategoria.length}/${LIMITE_NOMBRE} - ${MENSAJE_LIMITE}` + errores?.nombreCategoria || + `${nombreCategoria.length}/${LIMITE_NOMBRE} - ${MENSAJE_LIMITE}` } error={intentoEnviar && !!errores?.nombreCategoria} required @@ -103,10 +103,13 @@ const FormaCrearCategorias = ({ fullWidth type='text' value={descripcionCategoria} - onChange={(evento) => setDescripcionCategoria(evento.target.value.slice(0, LIMITE_DESCRIPCION))} + onChange={(evento) => + setDescripcionCategoria(evento.target.value.slice(0, LIMITE_DESCRIPCION)) + } inputProps={{ maxLength: LIMITE_DESCRIPCION }} helperText={ - errores?.descripcionCategoria || `${descripcionCategoria.length}/${LIMITE_DESCRIPCION} - ${MENSAJE_LIMITE}` + errores?.descripcionCategoria || + `${descripcionCategoria.length}/${LIMITE_DESCRIPCION} - ${MENSAJE_LIMITE}` } error={intentoEnviar && !!errores?.descripcionCategoria} required @@ -114,10 +117,8 @@ const FormaCrearCategorias = ({ multiline rows={3} /> - - {/* Alert moved to parent component */} ); }; -export default FormaCrearCategorias; \ No newline at end of file +export default FormaCrearCategorias; diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index 612d563c..c18a01c4 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -75,16 +75,6 @@ const ListaGrupoEmpleados = () => { const { actualizarGrupo } = useActualizarGrupoEmpleados(); const handleGuardar = async () => { - if (!formData?.isValid) { - setAlerta({ - tipo: 'warning', - mensaje: 'Por favor completa todos los campos requeridos', - icono: true, - cerrable: true, - centradoInferior: true, - }); - return; - } try { await actualizarGrupo( idGrupoSeleccionado, @@ -106,7 +96,7 @@ const ListaGrupoEmpleados = () => { } catch (error) { setAlerta({ tipo: 'error', - mensaje: 'Error al actualizar el grupo de empleados.', + mensaje: error?.message || 'Error al actualizar el grupo de empleados.', icono: true, cerrable: true, centradoInferior: true, diff --git a/src/hooks/Empleados/useActualizarGrupoEmpleados.js b/src/hooks/Empleados/useActualizarGrupoEmpleados.js index 0a6f7e8a..a2c6057a 100644 --- a/src/hooks/Empleados/useActualizarGrupoEmpleados.js +++ b/src/hooks/Empleados/useActualizarGrupoEmpleados.js @@ -7,17 +7,19 @@ import { RepositorioActualizarGrupoEmpleados } from '@Repositorios/Empleados/Rep */ export const useActualizarGrupoEmpleados = () => { const [mensaje, setMensaje] = useState(''); + const [exito, setExito] = useState(false); const [cargando, setCargando] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(false); const actualizarGrupo = async (idGrupo, nombre, descripcion, empleados, setsDeProductos) => { if (!idGrupo) return; - setCargando(true); - setError(null); + setExito(false); + setError(false); + setMensaje(''); try { - const { mensaje } = await RepositorioActualizarGrupoEmpleados.actualizarGrupoEmpleados( + const resultado = await RepositorioActualizarGrupoEmpleados.actualizarGrupoEmpleados( idGrupo, nombre, descripcion, @@ -25,15 +27,41 @@ export const useActualizarGrupoEmpleados = () => { setsDeProductos ); - setMensaje(mensaje); - return mensaje; + // Consideramos la actualización exitosa si tenemos una respuesta del servidor + setExito(true); + setMensaje(resultado?.data?.mensaje || 'Grupo actualizado exitosamente'); + return resultado; } catch (err) { - setError(err.message); + 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); } }; - return { actualizarGrupo, mensaje, cargando, error }; + const resetEstado = () => { + setExito(false); + setError(false); + setMensaje(''); + }; + + return { + actualizarGrupo, + cargando, + exito, + error, + mensaje, + setError, + resetEstado, + }; }; + +export default useActualizarGrupoEmpleados; From 6fa845b25dfe6405c17802fd154ec4d267c21b0f Mon Sep 17 00:00:00 2001 From: max Date: Mon, 2 Jun 2025 13:33:02 -0600 Subject: [PATCH 062/160] Refactor user update forms to improve validation handling and integrate FormularioActualizarUsuario component --- .../FormularioActualizarUsuario.jsx | 472 +++++++----------- .../Componentes/Organismos/ModalUsuarios.jsx | 85 +++- src/hooks/Usuarios/useAccionesUsuario.js | 7 + 3 files changed, 283 insertions(+), 281 deletions(-) diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormularioActualizarUsuario.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormularioActualizarUsuario.jsx index f12dbb83..384d31b3 100644 --- a/src/Vistas/Componentes/Organismos/Formularios/FormularioActualizarUsuario.jsx +++ b/src/Vistas/Componentes/Organismos/Formularios/FormularioActualizarUsuario.jsx @@ -1,296 +1,210 @@ -import { useState, useEffect } 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 Alerta from '@Moleculas/Alerta'; -import ModalFlotante from '@Organismos/ModalFlotante'; -import FormularioActualizarUsuario from '@Organismos/Formularios/FormularioActualizarUsuario'; -import { useConsultarClientes } from '@Hooks/Clientes/useConsultarClientes'; -import { useActualizarUsuario } from '@Hooks/Usuarios/useActualizarUsuario'; import CampoSelectMultiple from '@Atomos/CampoSelectMultiple'; -import { useAccionesUsuario } from '@Hooks/Usuarios/useAccionesUsuario'; - -const FormularioActualizarUsuario = ({ open, onClose, usuario, onUsuarioActualizado }) => { - const { - datosUsuario, - setDatosUsuario, - erroresValidacion, - alerta, - setAlerta, - cargando, - esEdicion, - manejarCambio, - manejarFechaNacimiento, - obtenerHelperText, - handleGuardar, - limpiarFormulario, - CAMPO_OBLIGATORIO, - } = useAccionesUsuario(usuarioInicial); // usuarioInicial es null para crear, o el usuario para editar - - useEffect(() => { - if (usuario) { - setDatosUsuario({ - nombreCompleto: usuario.nombreCompleto || '', - apellido: usuario.apellido || '', - correoElectronico: usuario.correoElectronico || '', - numeroTelefono: usuario.numeroTelefono || '', - direccion: usuario.direccion || '', - codigoPostal: usuario.codigoPostal || '', - fechaNacimiento: usuario.fechaNacimiento || null, - genero: usuario.genero || '', - cliente: usuario.cliente || [], - rol: usuario.rol || '', - contrasenia: '', - }); - } - }, [usuario]); - - const { errores, handleActualizarUsuario } = useActualizarUsuario(); - const { roles, cargando: cargandoRoles } = useConsultarRoles(); - const { clientes } = useConsultarClientes(); - const esSuperAdmin = datosUsuario.rol === 1; - - const manejarConfirmacion = async () => { - const resultado = await handleActualizarUsuario({ - ...datosUsuario, - idUsuario: usuario.idUsuario, - }); - - if (resultado?.mensaje) { - if (resultado.exito) { - setAlerta({ - tipo: 'success', - mensaje: `Usuario actualizado exitosamente.`, - icono: true, - cerrable: true, - centradoInferior: true, - duracion: 3000, - }); - setTimeout(async () => { - if (onUsuarioActualizado) await onUsuarioActualizado(); - manejarCierre(); - }, 2000); - } else { - setAlerta({ - tipo: 'error', - mensaje: resultado.mensaje, - }); - } - } - }; - - const manejarCierre = () => { - setAlerta(null); - onClose(); - }; +const FormularioActualizarUsuario = ({ + datosUsuario, + erroresValidacion, + manejarCambio, + manejarFechaNacimiento, + obtenerHelperText, + roles, + clientes, + esSuperAdmin, + cargandoRoles, + CAMPO_OBLIGATORIO, +}) => { const estiloCuadricula = { display: 'flex', justifyContent: 'center', }; return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - rol.idRol !== 3) - .map((rol) => ({ - value: rol.idRol, - label: rol.nombre, - }))} - disabled={cargandoRoles} - /> - - - ({ - value: cliente.idCliente, - label: cliente.nombreComercial, - }))} - disabled={esSuperAdmin} - /> - - - - - - - - {alerta && ( - setAlerta(null)} - centradoInferior - /> - )} - + + + + + + + + + + + + + + + + + + + + + + + + + + + rol.idRol !== 3) + .map((rol) => ({ + value: rol.idRol, + label: rol.nombre, + }))} + disabled={cargandoRoles} + /> + + + ({ + value: cliente.idCliente, + label: cliente.nombreComercial, + }))} + disabled={esSuperAdmin} + /> + + + + + + ); }; diff --git a/src/Vistas/Componentes/Organismos/ModalUsuarios.jsx b/src/Vistas/Componentes/Organismos/ModalUsuarios.jsx index f6a2a6ef..2892e6fa 100644 --- a/src/Vistas/Componentes/Organismos/ModalUsuarios.jsx +++ b/src/Vistas/Componentes/Organismos/ModalUsuarios.jsx @@ -1,5 +1,86 @@ import { Box } from '@mui/material'; import Alerta from '@Moleculas/Alerta'; import ModalFlotante from '@Organismos/ModalFlotante'; -import FormaEmpleado from '@Organismos/Formularios/FormaEmpleado'; -import { useLeerUsuario } from '@Hooks/Usuarios/useLeerUsuario'; +import FormularioActualizarUsuario from './Formularios/FormularioActualizarUsuario'; +import { useAccionesUsuario } from '@Hooks/Usuarios/useAccionesUsuario'; + +const ModalUsuarios = ({ open, onClose, onAccion, usuarioEdicion }) => { + const { + datosUsuario, + erroresValidacion, + alerta, + setAlerta, + cargando, + esEdicion, + manejarCambio, + manejarFechaNacimiento, + obtenerHelperText, + handleGuardar, + limpiarFormulario, + 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/hooks/Usuarios/useAccionesUsuario.js b/src/hooks/Usuarios/useAccionesUsuario.js index 08284b03..02ce67ca 100644 --- a/src/hooks/Usuarios/useAccionesUsuario.js +++ b/src/hooks/Usuarios/useAccionesUsuario.js @@ -20,8 +20,15 @@ export const useAccionesUsuario = (usuarioInicial = null) => { ...usuarioInicial, idUsuario: usuarioInicial.idUsuario || usuarioInicial.id, nombreCompleto: usuarioInicial.nombreCompleto || '', + apellido: usuarioInicial.apellido || '', correoElectronico: usuarioInicial.correoElectronico || '', + numeroTelefono: usuarioInicial.numeroTelefono || '', + direccion: usuarioInicial.direccion || '', fechaNacimiento, + genero: usuarioInicial.genero || '', + rol: usuarioInicial.rol || '', + cliente: usuarioInicial.cliente || [], + contrasenia: '', }; } return { From aa767db6ccf49817fdc87a052726c12a3ad0c067 Mon Sep 17 00:00:00 2001 From: angieriosc Date: Mon, 2 Jun 2025 13:57:13 -0600 Subject: [PATCH 063/160] Feat: Uso de iconos mui --- .../Moleculas/GrupoEmpleadosInfoEditable.jsx | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx index cab951af..16678991 100644 --- a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx +++ b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx @@ -1,4 +1,3 @@ -import Alerta from '@Moleculas/Alerta'; import CampoTexto from '@Atomos/CampoTexto'; import { useState, useEffect, useMemo } from 'react'; import obtenerSetsProductos from '@Servicios/obtenerSetsProductos'; @@ -18,6 +17,10 @@ import { Divider, } from '@mui/material'; import Texto from '@Atomos/Texto'; +import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; +import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft'; +import KeyboardDoubleArrowRightIcon from '@mui/icons-material/KeyboardDoubleArrowRight'; +import KeyboardDoubleArrowLeftIcon from '@mui/icons-material/KeyboardDoubleArrowLeft'; // Constantes para los límites de caracteres const LIMITE_NOMBRE = 50; @@ -399,9 +402,8 @@ const InfoGrupoEmpleadosEditable = ({ onClick={handleAllRightSets} disabled={leftSets.length === 0} aria-label='move all right' - > - ≫ - + startIcon={} + /> + startIcon={} + allign='center' + /> + startIcon={} + /> + startIcon={} + /> {customListSets('Sets Seleccionados', rightSets)} @@ -460,9 +460,8 @@ const InfoGrupoEmpleadosEditable = ({ onClick={handleAllRightEmpleados} disabled={leftEmpleados.length === 0} aria-label='move all right' - > - ≫ - + startIcon={} + /> + startIcon={} + /> + startIcon={} + /> + startIcon={} + /> {customListEmpleados('Empleados Seleccionados', rightEmpleados)} From 43f90c340356e8f8ae829bafc8a68682a6e750a1 Mon Sep 17 00:00:00 2001 From: NicoH00d Date: Mon, 2 Jun 2025 15:09:36 -0600 Subject: [PATCH 064/160] fix: documentacion de instrucciones --- .../Componentes/Moleculas/InfoProducto.jsx | 16 ++++++++++++---- .../Organismos/ModalImportarProductos.jsx | 9 ++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Vistas/Componentes/Moleculas/InfoProducto.jsx b/src/Vistas/Componentes/Moleculas/InfoProducto.jsx index 8cf0050e..347557d3 100644 --- a/src/Vistas/Componentes/Moleculas/InfoProducto.jsx +++ b/src/Vistas/Componentes/Moleculas/InfoProducto.jsx @@ -64,7 +64,9 @@ const InfoProducto = ({detalleProducto, imagenProducto}) => { Costo:{' '} - ${detalleProducto?.costo || 'No disponible'} + {detalleProducto?.costo !== undefined && detalleProducto?.costo !== null + ? `$${detalleProducto.costo}` + : 'No disponible'} @@ -72,7 +74,9 @@ const InfoProducto = ({detalleProducto, imagenProducto}) => { Precio de Venta:{' '} - ${detalleProducto?.precioVenta || 'No disponible'} + {detalleProducto?.precioVenta !== undefined && detalleProducto?.precioVenta !== null + ? `$${detalleProducto.precioVenta}` + : 'No disponible'} @@ -80,7 +84,9 @@ const InfoProducto = ({detalleProducto, imagenProducto}) => { Precio Cliente:{' '} - ${detalleProducto?.precioCliente || 'No disponible'} + {detalleProducto?.precioCliente !== undefined && detalleProducto?.precioCliente !== null + ? `$${detalleProducto.precioCliente}` + : 'No disponible'} @@ -88,7 +94,9 @@ const InfoProducto = ({detalleProducto, imagenProducto}) => { Precio en puntos:{' '} - {detalleProducto?.precioPuntos || 'No disponible'} + {detalleProducto?.precioPuntos !== undefined && detalleProducto?.precioPuntos !== null + ? `${detalleProducto.precioPuntos}` + : 'No disponible'} diff --git a/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx b/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx index 6a72c9c3..85339b54 100644 --- a/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx +++ b/src/Vistas/Componentes/Organismos/ModalImportarProductos.jsx @@ -178,14 +178,17 @@ const ModalImportarProductos = ({ abierto, onCerrar, onConfirm, cargando, errore

Importante:
Asegúrate de que 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. + Tambien, asegúrate de que la primera fila de cada producto no tenga campos vacíos, ya que el sistema la usa para identificar el producto principal. +

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:Identificador del proveedor del producto. Puede ser un campo vacío
nombreProducto, nombreComercial: Nombres básicos
descripcionProducto: Descripción del producto
- marca, modelo, tipoProducto
+ marca, modelo, tipoProducto: Datos básicos del producto
costo, precioVenta, precioCliente: Valores numéricos (usa punto decimal)
precioPuntos: Número entero
- impuesto, descuento: Porcentajes
+ impuesto, descuento: Valor numérico del porcentaje. Ejemplo: 16 (para 16%)
estado: 1 = activo, 0 = inactivo
envio: 1 = disponible, 0 = no disponible

Variante

@@ -197,7 +200,7 @@ const ModalImportarProductos = ({ abierto, onCerrar, onConfirm, cargando, errore SKUcomercial: Código visible al cliente
cantidad: Entero
costoAdicional: Número positivo
- descuentoOpcion: Porcentaje
+ descuentoOpcion: Valor numérico del porcentaje. Ejemplo: 50 (para 50%)
estadoOpcion: 1 = activa, 0 = inactiva

From 764b25ec9682b82eb93b211ca2c2b09080b577e9 Mon Sep 17 00:00:00 2001 From: angieriosc Date: Mon, 2 Jun 2025 16:51:55 -0600 Subject: [PATCH 065/160] Fix: Correciones eslint --- .../Moleculas/GrupoEmpleadosInfoEditable.jsx | 98 ++++++++----------- .../Formularios/FormaCrearCategoria.jsx | 8 +- src/Vistas/Componentes/Organismos/Tabla.jsx | 44 +-------- .../Paginas/Empleados/ListaGrupoEmpleados.jsx | 6 +- .../Empleados/useActualizarGrupoEmpleados.js | 10 +- 5 files changed, 59 insertions(+), 107 deletions(-) diff --git a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx index 16678991..16dc97d3 100644 --- a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx +++ b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx @@ -1,5 +1,5 @@ import CampoTexto from '@Atomos/CampoTexto'; -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import obtenerSetsProductos from '@Servicios/obtenerSetsProductos'; import obtenerEmpleados from '@Servicios/obtenerEmpleados'; import { useAuth } from '@Hooks/AuthProvider'; @@ -28,16 +28,16 @@ const LIMITE_DESCRIPCION = 150; const MENSAJE_LIMITE = 'Máximo caracteres'; // Funciones auxiliares para la lista de transferencia -function not(a, b) { - return a.filter((value) => !b.find((item) => item.id === value.id)); +function notInList(sourceList, compareList) { + return sourceList.filter((value) => !compareList.find((item) => item.id === value.id)); } -function intersection(a, b) { - return a.filter((value) => b.find((item) => item.id === value.id)); +function intersection(listOne, listTwo) { + return listOne.filter((value) => listTwo.find((item) => item.id === value.id)); } -function union(a, b) { - return [...a, ...not(b, a)]; +function union(listOne, listTwo) { + return [...listOne, ...notInList(listTwo, listOne)]; } const InfoGrupoEmpleadosEditable = ({ @@ -54,7 +54,6 @@ const InfoGrupoEmpleadosEditable = ({ const [nombre, setNombre] = useState(nombreInicial || ''); const [descripcion, setDescripcion] = useState(descripcionInicial || ''); const [errores, setErrores] = useState({}); - const [mostrarAlerta, setMostrarAlerta] = useState(false); // Estados para la lista de transferencia de empleados const [checkedEmpleados, setCheckedEmpleados] = useState([]); @@ -66,38 +65,28 @@ const InfoGrupoEmpleadosEditable = ({ const [leftSets, setLeftSets] = useState([]); const [rightSets, setRightSets] = useState(setsProductosInicial || []); - const rightSetsIds = useMemo( - () => - rightSets - .map((item) => item.id) - .sort() - .join(','), - [rightSets] - ); - - const rightEmpleadosIds = useMemo( - () => - rightEmpleados - .map((item) => item.id) - .sort() - .join(','), - [rightEmpleados] - ); - useEffect(() => { const obtenerDatos = async () => { const productos = await obtenerSetsProductos(clienteSeleccionado); - setLeftSets(productos.filter((prod) => !rightSets.find((r) => r.id === prod.id))); + setLeftSets( + productos.filter( + (producto) => !rightSets.find((selectedSet) => selectedSet.id === producto.id) + ) + ); const empleadosData = await obtenerEmpleados(clienteSeleccionado); - setLeftEmpleados(empleadosData.filter((emp) => !rightEmpleados.find((r) => r.id === emp.id))); + setLeftEmpleados( + empleadosData.filter( + (empleado) => !rightEmpleados.find((selectedEmp) => selectedEmp.id === empleado.id) + ) + ); }; obtenerDatos(); - }, [clienteSeleccionado]); + }, [clienteSeleccionado, rightSets, rightEmpleados]); // Validación de campos - const validarCampos = () => { + const validarCampos = useCallback(() => { const nuevosErrores = {}; if (!nombre.trim()) { @@ -114,22 +103,19 @@ const InfoGrupoEmpleadosEditable = ({ setErrores(nuevosErrores); return Object.keys(nuevosErrores).length === 0; - }; + }, [nombre, descripcion]); // Add dependencies that validarCampos uses // Efecto para validar y notificar cambios useEffect(() => { const isValid = validarCampos(); - - if (onFormDataChange) { - onFormDataChange({ - isValid, - nombre, - descripcion, - setsDeProductos: rightSets.map((set) => set.id), - empleados: rightEmpleados.map((emp) => emp.id), - }); - } - }, [nombre, descripcion, rightSets, rightEmpleados]); + onFormDataChange?.({ + isValid, + nombre, + descripcion, + setsDeProductos: rightSets.map((set) => set.id), + empleados: rightEmpleados.map((emp) => emp.id), + }); + }, [nombre, descripcion, rightSets, rightEmpleados, onFormDataChange, validarCampos]); // Handlers para empleados const handleToggleEmpleados = (value) => () => { @@ -158,15 +144,15 @@ const InfoGrupoEmpleadosEditable = ({ const handleCheckedRightEmpleados = () => { const leftChecked = intersection(checkedEmpleados, leftEmpleados); setRightEmpleados(rightEmpleados.concat(leftChecked)); - setLeftEmpleados(not(leftEmpleados, leftChecked)); - setCheckedEmpleados(not(checkedEmpleados, leftChecked)); + setLeftEmpleados(notInList(leftEmpleados, leftChecked)); + setCheckedEmpleados(notInList(checkedEmpleados, leftChecked)); }; const handleCheckedLeftEmpleados = () => { const rightChecked = intersection(checkedEmpleados, rightEmpleados); setLeftEmpleados(leftEmpleados.concat(rightChecked)); - setRightEmpleados(not(rightEmpleados, rightChecked)); - setCheckedEmpleados(not(checkedEmpleados, rightChecked)); + setRightEmpleados(notInList(rightEmpleados, rightChecked)); + setCheckedEmpleados(notInList(checkedEmpleados, rightChecked)); }; // Handlers para sets productos @@ -196,15 +182,15 @@ const InfoGrupoEmpleadosEditable = ({ const handleCheckedRightSets = () => { const leftChecked = intersection(checkedSets, leftSets); setRightSets(rightSets.concat(leftChecked)); - setLeftSets(not(leftSets, leftChecked)); - setCheckedSets(not(checkedSets, leftChecked)); + setLeftSets(notInList(leftSets, leftChecked)); + setCheckedSets(notInList(checkedSets, leftChecked)); }; const handleCheckedLeftSets = () => { const rightChecked = intersection(checkedSets, rightSets); setLeftSets(leftSets.concat(rightChecked)); - setRightSets(not(rightSets, rightChecked)); - setCheckedSets(not(checkedSets, rightChecked)); + setRightSets(notInList(rightSets, rightChecked)); + setCheckedSets(notInList(checkedSets, rightChecked)); }; const numberOfCheckedEmpleados = (items) => intersection(checkedEmpleados, items).length; @@ -212,7 +198,7 @@ const InfoGrupoEmpleadosEditable = ({ const handleToggleAllEmpleados = (items) => () => { if (numberOfCheckedEmpleados(items) === items.length) { - setCheckedEmpleados(not(checkedEmpleados, items)); + setCheckedEmpleados(notInList(checkedEmpleados, items)); } else { setCheckedEmpleados(union(checkedEmpleados, items)); } @@ -220,7 +206,7 @@ const InfoGrupoEmpleadosEditable = ({ const handleToggleAllSets = (items) => () => { if (numberOfCheckedSets(items) === items.length) { - setCheckedSets(not(checkedSets, items)); + setCheckedSets(notInList(checkedSets, items)); } else { setCheckedSets(union(checkedSets, items)); } @@ -235,8 +221,8 @@ const InfoGrupoEmpleadosEditable = ({ onClick={handleToggleAllEmpleados(items)} checked={numberOfCheckedEmpleados(items) === items.length && items.length !== 0} indeterminate={ - numberOfCheckedEmpleados(items) !== items.length && - numberOfCheckedEmpleados(items) !== 0 + numberOfCheckedEmpleados(items) !== items.length + && numberOfCheckedEmpleados(items) !== 0 } disabled={items.length === 0} inputProps={{ @@ -355,7 +341,7 @@ const InfoGrupoEmpleadosEditable = ({ variant='outlined' value={nombre} placeholder='Nombre del grupo' - onChange={(e) => setNombre(e.target.value)} + onChange={(event) => setNombre(event.target.value)} error={!!errores.nombre} helperText={errores.nombre || `${nombre.length}/${LIMITE_NOMBRE} ${MENSAJE_LIMITE}`} inputProps={{ maxLength: LIMITE_NOMBRE }} @@ -371,7 +357,7 @@ const InfoGrupoEmpleadosEditable = ({ variant='outlined' value={descripcion} placeholder='Escribe una descripción' - onChange={(e) => setDescripcion(e.target.value)} + onChange={(event) => setDescripcion(event.target.value)} error={!!errores.descripcion} helperText={ errores.descripcion || `${descripcion.length}/${LIMITE_DESCRIPCION} ${MENSAJE_LIMITE}` diff --git a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx index 9a8e4dc0..7bf9bba3 100644 --- a/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx +++ b/src/Vistas/Componentes/Organismos/Formularios/FormaCrearCategoria.jsx @@ -79,8 +79,8 @@ const FormaCrearCategorias = ({ onChange={(evento) => setNombreCategoria(evento.target.value.slice(0, LIMITE_NOMBRE))} inputProps={{ maxLength: LIMITE_NOMBRE }} helperText={ - errores?.nombreCategoria || - `${nombreCategoria.length}/${LIMITE_NOMBRE} - ${MENSAJE_LIMITE}` + errores?.nombreCategoria + || `${nombreCategoria.length}/${LIMITE_NOMBRE} - ${MENSAJE_LIMITE}` } error={intentoEnviar && !!errores?.nombreCategoria} required @@ -108,8 +108,8 @@ const FormaCrearCategorias = ({ } inputProps={{ maxLength: LIMITE_DESCRIPCION }} helperText={ - errores?.descripcionCategoria || - `${descripcionCategoria.length}/${LIMITE_DESCRIPCION} - ${MENSAJE_LIMITE}` + errores?.descripcionCategoria + || `${descripcionCategoria.length}/${LIMITE_DESCRIPCION} - ${MENSAJE_LIMITE}` } error={intentoEnviar && !!errores?.descripcionCategoria} required diff --git a/src/Vistas/Componentes/Organismos/Tabla.jsx b/src/Vistas/Componentes/Organismos/Tabla.jsx index b6dc0b2e..933bade5 100644 --- a/src/Vistas/Componentes/Organismos/Tabla.jsx +++ b/src/Vistas/Componentes/Organismos/Tabla.jsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { DataGrid, GridToolbar } from '@mui/x-data-grid'; +import { DataGrid } from '@mui/x-data-grid'; import { tokens, themeSettings } from '@SRC/theme'; import { styled } from '@mui/material/styles'; @@ -10,6 +10,8 @@ const spanishLocaleText = { columnMenuSortDesc: 'Ordenar descendente', columnMenuUnsort: 'Restablecer orden', columnMenuFilter: 'Filtrar', + columnMenuHideColumn: 'Ocultar columna', + columnMenuManageColumns: 'Mostrar columnas', noResultsOverlayLabel: 'No se encontraron resultados.', filterOperatorContains: 'Contiene', filterOperatorEquals: 'Es igual a', @@ -113,11 +115,6 @@ const StyledDataGrid = styled(DataGrid)(({ theme }) => { '& .MuiDataGrid-columnHeader, & .MuiDataGrid-cell': { outline: 'none', }, - - // Ocultar el separador de redimensionamiento de columnas - '& .MuiDataGrid-columnSeparator': { - display: 'none', - }, }; }); @@ -136,12 +133,6 @@ const Tabla = ({ pageSize: pageSize || 5, }); - const handleSelectionModelChange = (newSelection) => { - if (typeof onRowSelectionModelChange === 'function') { - onRowSelectionModelChange(newSelection); - } - }; - return ( { onRowSelectionModelChange(seleccion); }} @@ -160,29 +150,6 @@ const Tabla = ({ pagination localeText={spanishLocaleText} rowHeight={70} - disableColumnSelector - slots={{ - toolbar: GridToolbar, - }} - componentsProps={{ - toolbar: { - showQuickFilter: true, - quickFilterProps: { debounceMs: 500 }, - csvOptions: { disableToolbarButton: true }, - printOptions: { disableToolbarButton: true }, - }, - }} - initialState={{ - columns: { - columnVisibilityModel: {}, - }, - filter: { - filterModel: { - items: [], - }, - }, - }} - hideColumnsButton /> ); }; @@ -212,9 +179,8 @@ Tabla.defaultProps = { loading: false, pageSize: 5, onRowClick: () => {}, - onRowSelectionModelChange: undefined, - checkboxSelection: false, - disableRowSelectionOnClick: false, + onRowSelectionModelChange: () => {}, + disableRowSelectionOnClick: true, }; export default Tabla; diff --git a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx index c18a01c4..58690de0 100644 --- a/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx +++ b/src/Vistas/Paginas/Empleados/ListaGrupoEmpleados.jsx @@ -1,6 +1,6 @@ // 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, { use, useState } from 'react'; +import React, { useState } from 'react'; import Tabla from '@Organismos/Tabla'; import ContenedorLista from '@Organismos/ContenedorLista'; import { useConsultarGrupos } from '@Hooks/Empleados/useConsultarGrupos'; @@ -23,8 +23,8 @@ const ListaGrupoEmpleados = () => { const { usuario } = useAuth(); const theme = useTheme(); const colores = tokens(theme.palette.mode); - const MENSAJE_POPUP_ELIMINAR = - '¿Estás seguro de que deseas eliminar los grupos seleccionados? Esta acción no se puede deshacer.'; + const MENSAJE_POPUP_ELIMINAR + = '¿Estás seguro de que deseas eliminar los grupos seleccionados? Esta acción no se puede deshacer.'; const [modalCrearAbierto, setModalCrearAbierto] = useState(false); const [gruposSeleccionados, setGruposSeleccionados] = useState([]); diff --git a/src/hooks/Empleados/useActualizarGrupoEmpleados.js b/src/hooks/Empleados/useActualizarGrupoEmpleados.js index a2c6057a..a9c267ba 100644 --- a/src/hooks/Empleados/useActualizarGrupoEmpleados.js +++ b/src/hooks/Empleados/useActualizarGrupoEmpleados.js @@ -34,11 +34,11 @@ export const useActualizarGrupoEmpleados = () => { } 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'; + 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; From 96130d786c1eff88337680910c57d118e2d38dad Mon Sep 17 00:00:00 2001 From: angieriosc Date: Mon, 2 Jun 2025 22:02:32 -0600 Subject: [PATCH 066/160] =?UTF-8?q?Fix:=20correci=C3=B3n=20loop=20renderiz?= =?UTF-8?q?aci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Moleculas/GrupoEmpleadosInfoEditable.jsx | 562 ++++++++++-------- .../Paginas/Empleados/ListaGrupoEmpleados.jsx | 20 +- 2 files changed, 329 insertions(+), 253 deletions(-) diff --git a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx index 16dc97d3..a5d185af 100644 --- a/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx +++ b/src/Vistas/Componentes/Moleculas/GrupoEmpleadosInfoEditable.jsx @@ -1,5 +1,5 @@ import CampoTexto from '@Atomos/CampoTexto'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import obtenerSetsProductos from '@Servicios/obtenerSetsProductos'; import obtenerEmpleados from '@Servicios/obtenerEmpleados'; import { useAuth } from '@Hooks/AuthProvider'; @@ -29,15 +29,23 @@ const MENSAJE_LIMITE = 'Máximo caracteres'; // Funciones auxiliares para la lista de transferencia function notInList(sourceList, compareList) { - return sourceList.filter((value) => !compareList.find((item) => item.id === value.id)); + return sourceList.filter( + (item) => !compareList.find((compareItem) => compareItem.id === item.id) + ); } function intersection(listOne, listTwo) { - return listOne.filter((value) => listTwo.find((item) => item.id === value.id)); + return listOne.filter((item) => listTwo.find((item2) => item2.id === item.id)); } function union(listOne, listTwo) { - return [...listOne, ...notInList(listTwo, listOne)]; + const combined = [...listOne]; + listTwo.forEach((item) => { + if (!combined.find((existingItem) => existingItem.id === item.id)) { + combined.push(item); + } + }); + return combined; } const InfoGrupoEmpleadosEditable = ({ @@ -50,286 +58,347 @@ const InfoGrupoEmpleadosEditable = ({ const { usuario } = useAuth(); const clienteSeleccionado = usuario.clienteSeleccionado; + // Ref para evitar llamadas múltiples del callback + const isInitializedRef = useRef(false); + const lastFormDataRef = useRef(null); + // Estados locales const [nombre, setNombre] = useState(nombreInicial || ''); const [descripcion, setDescripcion] = useState(descripcionInicial || ''); - const [errores, setErrores] = useState({}); + + // Estados para datos completos (sin filtrar) + const [todosLosSets, setTodosLosSets] = useState([]); + const [todosLosEmpleados, setTodosLosEmpleados] = useState([]); + const [datosListos, setDatosListos] = useState(false); // Estados para la lista de transferencia de empleados const [checkedEmpleados, setCheckedEmpleados] = useState([]); - const [leftEmpleados, setLeftEmpleados] = useState([]); const [rightEmpleados, setRightEmpleados] = useState(empleadosInicial || []); // Estados para la lista de transferencia de sets productos const [checkedSets, setCheckedSets] = useState([]); - const [leftSets, setLeftSets] = useState([]); const [rightSets, setRightSets] = useState(setsProductosInicial || []); + // Carga inicial de datos useEffect(() => { const obtenerDatos = async () => { - const productos = await obtenerSetsProductos(clienteSeleccionado); - setLeftSets( - productos.filter( - (producto) => !rightSets.find((selectedSet) => selectedSet.id === producto.id) - ) - ); - - const empleadosData = await obtenerEmpleados(clienteSeleccionado); - setLeftEmpleados( - empleadosData.filter( - (empleado) => !rightEmpleados.find((selectedEmp) => selectedEmp.id === empleado.id) - ) - ); + try { + const [productos, empleadosData] = await Promise.all([ + obtenerSetsProductos(clienteSeleccionado), + obtenerEmpleados(clienteSeleccionado), + ]); + + setTodosLosSets(productos); + setTodosLosEmpleados(empleadosData); + setDatosListos(true); + isInitializedRef.current = true; + } catch (error) { + console.error('Error al obtener datos:', error); + } }; - obtenerDatos(); - }, [clienteSeleccionado, rightSets, rightEmpleados]); - - // Validación de campos - const validarCampos = useCallback(() => { - const nuevosErrores = {}; - - if (!nombre.trim()) { - nuevosErrores.nombre = 'Este campo es obligatorio'; - } else if (nombre.length > LIMITE_NOMBRE) { - nuevosErrores.nombre = `El nombre no puede exceder ${LIMITE_NOMBRE} caracteres`; + if (clienteSeleccionado && !isInitializedRef.current) { + obtenerDatos(); } + }, [clienteSeleccionado]); + + // Calcular listas filtradas como valores derivados + const leftSets = useMemo( + () => + todosLosSets.filter( + (producto) => !rightSets.find((selectedSet) => selectedSet.id === producto.id) + ), + [todosLosSets, rightSets] + ); - if (!descripcion.trim()) { - nuevosErrores.descripcion = 'Este campo es obligatorio'; - } else if (descripcion.length > LIMITE_DESCRIPCION) { - nuevosErrores.descripcion = `La descripción no puede exceder ${LIMITE_DESCRIPCION} caracteres`; - } + const leftEmpleados = useMemo( + () => + todosLosEmpleados.filter( + (empleado) => !rightEmpleados.find((selectedEmp) => selectedEmp.id === empleado.id) + ), + [todosLosEmpleados, rightEmpleados] + ); - setErrores(nuevosErrores); - return Object.keys(nuevosErrores).length === 0; - }, [nombre, descripcion]); // Add dependencies that validarCampos uses + // Función estable para generar los datos del formulario + const generateFormData = useCallback(() => { + if (!datosListos) return null; - // Efecto para validar y notificar cambios - useEffect(() => { - const isValid = validarCampos(); - onFormDataChange?.({ - isValid, - nombre, - descripcion, + return { + nombre: nombre.trim(), + descripcion: descripcion.trim(), setsDeProductos: rightSets.map((set) => set.id), empleados: rightEmpleados.map((emp) => emp.id), - }); - }, [nombre, descripcion, rightSets, rightEmpleados, onFormDataChange, validarCampos]); - - // Handlers para empleados - const handleToggleEmpleados = (value) => () => { - const currentIndex = checkedEmpleados.findIndex((item) => item.id === value.id); - const newChecked = [...checkedEmpleados]; - - if (currentIndex === -1) { - newChecked.push(value); - } else { - newChecked.splice(currentIndex, 1); + }; + }, [nombre, descripcion, rightSets, rightEmpleados, datosListos]); + + // Efecto para notificar cambios en el formulario con debounce y comparación + useEffect(() => { + if (!datosListos || !onFormDataChange) return; + + const formData = generateFormData(); + + // Comparar con los datos anteriores para evitar llamadas innecesarias + const formDataString = JSON.stringify(formData); + const lastFormDataString = JSON.stringify(lastFormDataRef.current); + + if (formDataString !== lastFormDataString) { + lastFormDataRef.current = formData; + + // Usar setTimeout para debounce y evitar llamadas síncronas que causen loops + const timeoutId = setTimeout(() => { + onFormDataChange(formData); + }, 0); + + return () => clearTimeout(timeoutId); } + }, [generateFormData, onFormDataChange, datosListos]); + + // Handlers para empleados - estabilizados + const handleToggleEmpleados = useCallback( + (value) => () => { + setCheckedEmpleados((prev) => { + const currentIndex = prev.findIndex((item) => item.id === value.id); + const newChecked = [...prev]; + + if (currentIndex === -1) { + newChecked.push(value); + } else { + newChecked.splice(currentIndex, 1); + } - setCheckedEmpleados(newChecked); - }; + return newChecked; + }); + }, + [] + ); - const handleAllLeftEmpleados = () => { - setLeftEmpleados(leftEmpleados.concat(rightEmpleados)); + const handleAllLeftEmpleados = useCallback(() => { setRightEmpleados([]); - }; + setCheckedEmpleados([]); + }, []); - const handleAllRightEmpleados = () => { - setRightEmpleados(rightEmpleados.concat(leftEmpleados)); - setLeftEmpleados([]); - }; + const handleAllRightEmpleados = useCallback(() => { + setRightEmpleados((prev) => [...prev, ...leftEmpleados]); + setCheckedEmpleados([]); + }, [leftEmpleados]); - const handleCheckedRightEmpleados = () => { + const handleCheckedRightEmpleados = useCallback(() => { const leftChecked = intersection(checkedEmpleados, leftEmpleados); - setRightEmpleados(rightEmpleados.concat(leftChecked)); - setLeftEmpleados(notInList(leftEmpleados, leftChecked)); - setCheckedEmpleados(notInList(checkedEmpleados, leftChecked)); - }; + setRightEmpleados((prev) => [...prev, ...leftChecked]); + setCheckedEmpleados((prev) => notInList(prev, leftChecked)); + }, [checkedEmpleados, leftEmpleados]); - const handleCheckedLeftEmpleados = () => { + const handleCheckedLeftEmpleados = useCallback(() => { const rightChecked = intersection(checkedEmpleados, rightEmpleados); - setLeftEmpleados(leftEmpleados.concat(rightChecked)); - setRightEmpleados(notInList(rightEmpleados, rightChecked)); - setCheckedEmpleados(notInList(checkedEmpleados, rightChecked)); - }; - - // Handlers para sets productos - const handleToggleSets = (value) => () => { - const currentIndex = checkedSets.findIndex((item) => item.id === value.id); - const newChecked = [...checkedSets]; - - if (currentIndex === -1) { - newChecked.push(value); - } else { - newChecked.splice(currentIndex, 1); - } + setRightEmpleados((prev) => notInList(prev, rightChecked)); + setCheckedEmpleados((prev) => notInList(prev, rightChecked)); + }, [checkedEmpleados, rightEmpleados]); + + // Handlers para sets productos - estabilizados + const handleToggleSets = useCallback( + (value) => () => { + setCheckedSets((prev) => { + const currentIndex = prev.findIndex((item) => item.id === value.id); + const newChecked = [...prev]; + + if (currentIndex === -1) { + newChecked.push(value); + } else { + newChecked.splice(currentIndex, 1); + } - setCheckedSets(newChecked); - }; + return newChecked; + }); + }, + [] + ); - const handleAllLeftSets = () => { - setLeftSets(leftSets.concat(rightSets)); + const handleAllLeftSets = useCallback(() => { setRightSets([]); - }; + setCheckedSets([]); + }, []); - const handleAllRightSets = () => { - setRightSets(rightSets.concat(leftSets)); - setLeftSets([]); - }; + const handleAllRightSets = useCallback(() => { + setRightSets((prev) => [...prev, ...leftSets]); + setCheckedSets([]); + }, [leftSets]); - const handleCheckedRightSets = () => { + const handleCheckedRightSets = useCallback(() => { const leftChecked = intersection(checkedSets, leftSets); - setRightSets(rightSets.concat(leftChecked)); - setLeftSets(notInList(leftSets, leftChecked)); - setCheckedSets(notInList(checkedSets, leftChecked)); - }; + setRightSets((prev) => [...prev, ...leftChecked]); + setCheckedSets((prev) => notInList(prev, leftChecked)); + }, [checkedSets, leftSets]); - const handleCheckedLeftSets = () => { + const handleCheckedLeftSets = useCallback(() => { const rightChecked = intersection(checkedSets, rightSets); - setLeftSets(leftSets.concat(rightChecked)); - setRightSets(notInList(rightSets, rightChecked)); - setCheckedSets(notInList(checkedSets, rightChecked)); - }; - - const numberOfCheckedEmpleados = (items) => intersection(checkedEmpleados, items).length; - const numberOfCheckedSets = (items) => intersection(checkedSets, items).length; - - const handleToggleAllEmpleados = (items) => () => { - if (numberOfCheckedEmpleados(items) === items.length) { - setCheckedEmpleados(notInList(checkedEmpleados, items)); - } else { - setCheckedEmpleados(union(checkedEmpleados, items)); - } - }; + setRightSets((prev) => notInList(prev, rightChecked)); + setCheckedSets((prev) => notInList(prev, rightChecked)); + }, [checkedSets, rightSets]); + + // Funciones auxiliares memoizadas + const numberOfCheckedEmpleados = useMemo( + () => (items) => intersection(checkedEmpleados, items).length, + [checkedEmpleados] + ); - const handleToggleAllSets = (items) => () => { - if (numberOfCheckedSets(items) === items.length) { - setCheckedSets(notInList(checkedSets, items)); - } else { - setCheckedSets(union(checkedSets, items)); - } - }; - - const customListEmpleados = (title, items) => ( - - - } - title={title} - subheader={`${numberOfCheckedEmpleados(items)}/${items.length} seleccionados`} - /> - - - {items.map((value) => { - const labelId = `transfer-list-empleados-${value.id}-label`; - - return ( - - - item.id === value.id)} - tabIndex={-1} - disableRipple - inputProps={{ - 'aria-labelledby': labelId, - }} + const numberOfCheckedSets = useMemo( + () => (items) => intersection(checkedSets, items).length, + [checkedSets] + ); + + const handleToggleAllEmpleados = useCallback( + (items) => () => { + if (numberOfCheckedEmpleados(items) === items.length) { + setCheckedEmpleados((prev) => notInList(prev, items)); + } else { + setCheckedEmpleados((prev) => union(prev, items)); + } + }, + [numberOfCheckedEmpleados] + ); + + const handleToggleAllSets = useCallback( + (items) => () => { + if (numberOfCheckedSets(items) === items.length) { + setCheckedSets((prev) => notInList(prev, items)); + } else { + setCheckedSets((prev) => union(prev, items)); + } + }, + [numberOfCheckedSets] + ); + + // Componentes de lista memoizados + const customListEmpleados = useCallback( + (title, items) => ( + + + } + title={title} + subheader={`${numberOfCheckedEmpleados(items)}/${items.length} seleccionados`} + /> + + + {items.map((value) => { + const labelId = `transfer-list-empleados-${value.id}-label`; + + return ( + + + item.id === value.id)} + tabIndex={-1} + disableRipple + inputProps={{ + 'aria-labelledby': labelId, + }} + /> + + - - - - ); - })} - - + + ); + })} + +
+ ), + [checkedEmpleados, handleToggleEmpleados, handleToggleAllEmpleados, numberOfCheckedEmpleados] ); - const customListSets = (title, items) => ( - - - } - title={title} - subheader={`${numberOfCheckedSets(items)}/${items.length} seleccionados`} - /> - - - {items.map((value) => { - const labelId = `transfer-list-sets-${value.id}-label`; - - return ( - - - item.id === value.id)} - tabIndex={-1} - disableRipple - inputProps={{ - 'aria-labelledby': labelId, - }} + + const customListSets = useCallback( + (title, items) => ( + + + } + title={title} + subheader={`${numberOfCheckedSets(items)}/${items.length} seleccionados`} + /> + + + {items.map((value) => { + const labelId = `transfer-list-sets-${value.id}-label`; + + return ( + + + item.id === value.id)} + tabIndex={-1} + disableRipple + inputProps={{ + 'aria-labelledby': labelId, + }} + /> + + - - - - ); - })} - - + + ); + })} + + + ), + [checkedSets, handleToggleSets, handleToggleAllSets, numberOfCheckedSets] ); + // Mostrar loading mientras se cargan los datos + if (!datosListos) { + return
Cargando datos...
; + } + return ( @@ -338,12 +407,13 @@ const InfoGrupoEmpleadosEditable = ({ Nombre: setNombre(event.target.value)} - error={!!errores.nombre} - helperText={errores.nombre || `${nombre.length}/${LIMITE_NOMBRE} ${MENSAJE_LIMITE}`} + onChange={(evento) => setNombre(evento.target.value.slice(0, LIMITE_NOMBRE))} + required + helperText={`${nombre.length}/${LIMITE_NOMBRE} ${MENSAJE_LIMITE}`} inputProps={{ maxLength: LIMITE_NOMBRE }} sx={{ mt: 1, mb: 2, mr: 8, width: '300px', overflow: 'auto' }} /> @@ -357,11 +427,8 @@ const InfoGrupoEmpleadosEditable = ({ variant='outlined' value={descripcion} placeholder='Escribe una descripción' - onChange={(event) => setDescripcion(event.target.value)} - error={!!errores.descripcion} - helperText={ - errores.descripcion || `${descripcion.length}/${LIMITE_DESCRIPCION} ${MENSAJE_LIMITE}` - } + onChange={(evento) => setDescripcion(evento.target.value.slice(0, LIMITE_DESCRIPCION))} + helperText={`${descripcion.length}/${LIMITE_DESCRIPCION} ${MENSAJE_LIMITE}`} inputProps={{ maxLength: LIMITE_DESCRIPCION }} sx={{ mt: 1, mb: 2, width: '400px', overflow: 'auto' }} /> @@ -398,7 +465,6 @@ const InfoGrupoEmpleadosEditable = ({ disabled={numberOfCheckedSets(leftSets) === 0} aria-label='move selected right' startIcon={} - allign='center' />