From b6ba52f074807c7a2fddc76ab3cc2c45c446c1f8 Mon Sep 17 00:00:00 2001 From: eldiadmolinari Date: Tue, 20 May 2025 12:38:55 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Implementaci=C3=B3n=20CRUD=20Canillitas?= =?UTF-8?q?,=20Distribuidores=20y=20Precios=20de=20Publicaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend API: - Canillitas (`dist_dtCanillas`): - Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador). - Lógica para manejo de `Accionista`, `Baja`, `FechaBaja`. - Auditoría en `dist_dtCanillas_H`. - Validación de legajo único y lógica de empresa vs accionista. - Distribuidores (`dist_dtDistribuidores`): - Implementado CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador). - Auditoría en `dist_dtDistribuidores_H`. - Creación de saldos iniciales para el nuevo distribuidor en todas las empresas. - Verificación de NroDoc único y Nombre opcionalmente único. - Precios de Publicación (`dist_Precios`): - Implementado CRUD básico (Modelos, DTOs, Repositorio, Servicio, Controlador). - Endpoints anidados bajo `/publicaciones/{idPublicacion}/precios`. - Lógica de negocio para cerrar período de precio anterior al crear uno nuevo. - Lógica de negocio para reabrir período de precio anterior al eliminar el último. - Auditoría en `dist_Precios_H`. - Auditoría en Eliminación de Publicaciones: - Extendido `PublicacionService.EliminarAsync` para eliminar en cascada registros de precios, recargos, porcentajes de pago (distribuidores y canillitas) y secciones de publicación. - Repositorios correspondientes (`PrecioRepository`, `RecargoZonaRepository`, `PorcPagoRepository`, `PorcMonCanillaRepository`, `PubliSeccionRepository`) actualizados con métodos `DeleteByPublicacionIdAsync` que registran en sus respectivas tablas `_H` (si existen y se implementó la lógica). - Asegurada la correcta propagación del `idUsuario` para la auditoría en cascada. - Correcciones de Nulabilidad: - Ajustados los métodos `MapToDto` y su uso en `CanillaService` y `PublicacionService` para manejar correctamente tipos anulables. Frontend React: - Canillitas: - `canillaService.ts`. - `CanillaFormModal.tsx` con selectores para Zona y Empresa, y lógica de Accionista. - `GestionarCanillitasPage.tsx` con filtros, paginación, y acciones (editar, toggle baja). - Distribuidores: - `distribuidorService.ts`. - `DistribuidorFormModal.tsx` con múltiples campos y selector de Zona. - `GestionarDistribuidoresPage.tsx` con filtros, paginación, y acciones (editar, eliminar). - Precios de Publicación: - `precioService.ts`. - `PrecioFormModal.tsx` para crear/editar períodos de precios (VigenciaD, VigenciaH opcional, precios por día). - `GestionarPreciosPublicacionPage.tsx` accesible desde la gestión de publicaciones, para listar y gestionar los períodos de precios de una publicación específica. - Layout: - Reemplazado el uso de `Grid` por `Box` con Flexbox en `CanillaFormModal`, `GestionarCanillitasPage` (filtros), `DistribuidorFormModal` y `PrecioFormModal` para resolver problemas de tipos y mejorar la consistencia del layout de formularios. - Navegación: - Actualizadas las rutas y pestañas para los nuevos módulos y sub-módulos. --- .../Distribucion/CanillasController.cs | 133 ++++++++ .../Distribucion/DistribuidoresController.cs | 120 +++++++ .../Distribucion/OtrosDestinosController.cs | 180 +++++++++++ .../Distribucion/PreciosController.cs | 144 +++++++++ .../Distribucion/PublicacionesController.cs | 121 ++++++++ .../{ => Usuarios}/AuthController.cs | 4 +- .../Usuarios/PerfilesController.cs | 239 ++++++++++++++ .../Usuarios/PermisosController.cs | 153 +++++++++ .../Usuarios/UsuariosController.cs | 158 ++++++++++ .../Distribucion/CanillaRepository.cs | 205 ++++++++++++ .../Distribucion/DistribuidorRepository.cs | 212 +++++++++++++ .../Distribucion/ICanillaRepository.cs | 19 ++ .../Distribucion/IDistribuidorRepository.cs | 20 ++ .../Distribucion/IOtroDestinoRepository.cs | 18 ++ .../Distribucion/IPorcMonCanillaRepository.cs | 10 + .../Distribucion/IPorcPagoRepository.cs | 9 + .../Distribucion/IPrecioRepository.cs | 20 ++ .../Distribucion/IPubliSeccionRepository.cs | 10 + .../Distribucion/IPublicacionRepository.cs | 19 ++ .../Distribucion/IRecargoZonaRepository.cs | 10 + .../Distribucion/OtroDestinoRepository.cs | 189 +++++++++++ .../Distribucion/PorcMonCanillaRepository.cs | 58 ++++ .../Distribucion/PorcPagoRepository.cs | 56 ++++ .../Distribucion/PrecioRepository.cs | 177 +++++++++++ .../Distribucion/PubliSeccionRepository.cs | 55 ++++ .../Distribucion/PublicacionRepository.cs | 219 +++++++++++++ .../Distribucion/RecargoZonaRepository.cs | 64 ++++ .../Usuarios}/AuthRepository.cs | 4 +- .../Usuarios}/IAuthRepository.cs | 4 +- .../Usuarios/IPerfilRepository.cs | 20 ++ .../Usuarios/IPermisoRepository.cs | 19 ++ .../Usuarios/IUsuarioRepository.cs | 25 ++ .../Repositories/Usuarios/PerfilRepository.cs | 250 +++++++++++++++ .../Usuarios/PermisoRepository.cs | 231 ++++++++++++++ .../Usuarios/UsuarioRepository.cs | 293 ++++++++++++++++++ .../Models/Distribucion/Canilla.cs | 16 + .../Models/Distribucion/CanillaHistorico.cs | 21 ++ .../Models/Distribucion/Distribuidor.cs | 18 ++ .../Distribucion/DistribuidorHistorico.cs | 23 ++ .../EmpresaHistorico.cs | 0 .../Models/Distribucion/OtroDestino.cs | 14 + .../Distribucion/OtroDestinoHistorico.cs | 19 ++ .../Models/Distribucion/PorcMonCanilla.cs | 15 + .../Distribucion/PorcMonCanillaHistorico.cs | 18 ++ .../Models/Distribucion/PorcPago.cs | 14 + .../Models/Distribucion/PorcPagoHistorico.cs | 16 + .../Models/Distribucion/Precio.cs | 17 + .../Models/Distribucion/PrecioHistorico.cs | 22 ++ .../Models/Distribucion/PubliSeccion.cs | 10 + .../Distribucion/PubliSeccionHistorico.cs | 15 + .../Models/Distribucion/Publicacion.cs | 12 + .../Distribucion/PublicacionHistorico.cs | 17 + .../Models/Distribucion/RecargoZona.cs | 14 + .../Distribucion/RecargoZonaHistorico.cs | 15 + .../Models/Dtos/Distribucion/CanillaDto.cs | 18 ++ .../Dtos/Distribucion/CreateCanillaDto.cs | 33 ++ .../Distribucion/CreateDistribuidorDto.cs | 36 +++ .../CreateEmpresaDto.cs | 0 .../Dtos/Distribucion/CreateOtroDestinoDto.cs | 14 + .../Dtos/Distribucion/CreatePrecioDto.cs | 30 ++ .../Dtos/Distribucion/CreatePublicacionDto.cs | 23 ++ .../{Zonas => Distribucion}/CreateZonaDto.cs | 0 .../Dtos/Distribucion/DistribuidorDto.cs | 19 ++ .../{Empresas => Distribucion}/EmpresaDto.cs | 0 .../Dtos/Distribucion/OtroDestinoDto.cs | 9 + .../Models/Dtos/Distribucion/PrecioDto.cs | 17 + .../Dtos/Distribucion/PublicacionDto.cs | 13 + .../Dtos/Distribucion/ToggleBajaCanillaDto.cs | 10 + .../Dtos/Distribucion/UpdateCanillaDto.cs | 31 ++ .../Distribucion/UpdateDistribuidorDto.cs | 36 +++ .../UpdateEmpresaDto.cs | 0 .../Dtos/Distribucion/UpdateOtroDestinoDto.cs | 14 + .../Dtos/Distribucion/UpdatePrecioDto.cs | 28 ++ .../Dtos/Distribucion/UpdatePublicacionDto.cs | 24 ++ .../{Zonas => Distribucion}/UpdateZonaDto.cs | 0 .../Dtos/{Zonas => Distribucion}/ZonaDto.cs | 0 .../ActualizarPermisosPerfilRequestDto.cs | 12 + .../ChangePasswordRequestDto.cs | 0 .../Models/Dtos/Usuarios/CreatePerfilDto.cs | 14 + .../Models/Dtos/Usuarios/CreatePermisoDto.cs | 20 ++ .../Dtos/Usuarios/CreateUsuarioRequestDto.cs | 34 ++ .../Dtos/{ => Usuarios}/LoginRequestDto.cs | 0 .../Dtos/{ => Usuarios}/LoginResponseDto.cs | 0 .../Models/Dtos/Usuarios/PerfilDto.cs | 9 + .../Dtos/Usuarios/PermisoAsignadoDto.cs | 11 + .../Models/Dtos/Usuarios/PermisoDto.cs | 10 + .../Dtos/Usuarios/SetPasswordRequestDto.cs | 11 + .../Models/Dtos/Usuarios/UpdatePerfilDto.cs | 14 + .../Models/Dtos/Usuarios/UpdatePermisoDto.cs | 19 ++ .../Dtos/Usuarios/UpdateUsuarioRequestDto.cs | 34 ++ .../Models/Dtos/Usuarios/UsuarioDto.cs | 16 + .../Models/Usuarios/Perfil.cs | 14 + .../Models/Usuarios/PerfilHistorico.cs | 12 + .../Models/Usuarios/Permiso.cs | 17 + .../Models/Usuarios/PermisoHistorico.cs | 14 + .../Models/{ => Usuarios}/Usuario.cs | 2 +- .../Models/Usuarios/UsuarioHistorico.cs | 33 ++ Backend/GestionIntegral.Api/Program.cs | 23 +- .../Services/Distribucion/CanillaService.cs | 255 +++++++++++++++ .../Distribucion/DistribuidorService.cs | 197 ++++++++++++ .../Services/Distribucion/ICanillaService.cs | 15 + .../Distribucion/IDistribuidorService.cs | 15 + .../Distribucion/IOtroDestinoService.cs | 15 + .../Services/Distribucion/IPrecioService.cs | 16 + .../Distribucion/IPublicacionService.cs | 15 + .../Distribucion/OtroDestinoService.cs | 143 +++++++++ .../Services/Distribucion/PrecioService.cs | 230 ++++++++++++++ .../Distribucion/PublicacionService.cs | 206 ++++++++++++ .../Services/{ => Usuarios}/AuthService.cs | 6 +- .../Services/{ => Usuarios}/IAuthService.cs | 2 +- .../Services/Usuarios/IPerfilService.cs | 17 + .../Services/Usuarios/IPermisoService.cs | 15 + .../Services/Usuarios/IUsuarioService.cs | 18 ++ .../{ => Usuarios}/PasswordHasherService.cs | 2 +- .../Services/Usuarios/PerfilService.cs | 225 ++++++++++++++ .../Services/Usuarios/PermisoService.cs | 163 ++++++++++ .../Services/Usuarios/UsuarioService.cs | 246 +++++++++++++++ .../appsettings.Development.json | 2 +- .../Debug/net9.0/appsettings.Development.json | 2 +- .../GestionIntegral.Api.AssemblyInfo.cs | 2 +- ...onIntegral.Api.csproj.FileListAbsolute.txt | 7 +- .../obj/Debug/net9.0/rbcswa.dswa.cache.json | 1 + .../Debug/net9.0/rjsmcshtml.dswa.cache.json | 1 + .../Debug/net9.0/rjsmrazor.dswa.cache.json | 1 + .../obj/Debug/net9.0/rpswa.dswa.cache.json | 1 + .../msbuild.build.GestionIntegral.Api.props | 4 - ...ldMultiTargeting.GestionIntegral.Api.props | 3 - ....buildTransitive.GestionIntegral.Api.props | 3 - ...stionIntegral.Api.csproj.nuget.dgspec.json | 17 +- .../GestionIntegral.Api.csproj.nuget.g.props | 6 +- .../obj/project.assets.json | 9 +- .../{ => Contables}/TipoPagoFormModal.tsx | 4 +- .../Modals/Distribucion/CanillaFormModal.tsx | 239 ++++++++++++++ .../Distribucion/DistribuidorFormModal.tsx | 239 ++++++++++++++ .../{ => Distribucion}/EmpresaFormModal.tsx | 6 +- .../Distribucion/OtroDestinoFormModal.tsx | 128 ++++++++ .../Modals/Distribucion/PrecioFormModal.tsx | 225 ++++++++++++++ .../Distribucion/PublicacionFormModal.tsx | 180 +++++++++++ .../{ => Distribucion}/ZonaFormModal.tsx | 4 +- .../{ => Impresion}/EstadoBobinaFormModal.tsx | 6 +- .../{ => Impresion}/PlantaFormModal.tsx | 6 +- .../{ => Impresion}/TipoBobinaFormModal.tsx | 6 +- .../Usuarios}/ChangePasswordModal.tsx | 6 +- .../Modals/Usuarios/PerfilFormModal.tsx | 128 ++++++++ .../Modals/Usuarios/PermisoFormModal.tsx | 135 ++++++++ .../Modals/Usuarios/PermisosChecklist.tsx | 69 +++++ .../Modals/Usuarios/SetPasswordModal.tsx | 122 ++++++++ .../Modals/Usuarios/UsuarioFormModal.tsx | 255 +++++++++++++++ Frontend/src/contexts/AuthContext.tsx | 2 +- Frontend/src/layouts/MainLayout.tsx | 2 +- .../models/dtos/Distribucion/CanillaDto.ts | 14 + .../dtos/Distribucion/CreateCanillaDto.ts | 9 + .../Distribucion/CreateDistribuidorDto.ts | 13 + .../CreateEmpresaDto.ts | 0 .../dtos/Distribucion/CreateOtroDestinoDto.ts | 4 + .../dtos/Distribucion/CreatePrecioDto.ts | 12 + .../dtos/Distribucion/CreatePublicacionDto.ts | 7 + .../dtos/Distribucion/DistribuidorDto.ts | 15 + .../{Empresas => Distribucion}/EmpresaDto.ts | 0 .../dtos/Distribucion/OtroDestinoDto.ts | 5 + .../src/models/dtos/Distribucion/PrecioDto.ts | 13 + .../dtos/Distribucion/PublicacionDto.ts | 9 + .../dtos/Distribucion/ToggleBajaCanillaDto.ts | 3 + .../dtos/Distribucion/UpdateCanillaDto.ts | 9 + .../Distribucion/UpdateDistribuidorDto.ts | 13 + .../UpdateEmpresaDto.ts | 0 .../dtos/Distribucion/UpdateOtroDestinoDto.ts | 4 + .../dtos/Distribucion/UpdatePrecioDto.ts | 11 + .../dtos/Distribucion/UpdatePublicacionDto.ts | 7 + Frontend/src/models/dtos/LoginRequestDto.ts | 5 - .../ActualizarPermisosPerfilRequestDto.ts | 3 + .../ChangePasswordRequestDto.ts | 1 - .../models/dtos/Usuarios/CreatePerfilDto.ts | 4 + .../models/dtos/Usuarios/CreatePermisoDto.ts | 5 + .../dtos/Usuarios/CreateUsuarioRequestDto.ts | 11 + .../models/dtos/Usuarios/LoginRequestDto.ts | 4 + .../dtos/{ => Usuarios}/LoginResponseDto.ts | 2 - .../src/models/dtos/Usuarios/PerfilDto.ts | 5 + .../dtos/Usuarios/PermisoAsignadoDto.ts | 7 + .../src/models/dtos/Usuarios/PermisoDto.ts | 7 + .../dtos/Usuarios/SetPasswordRequestDto.ts | 4 + .../models/dtos/Usuarios/UpdatePerfilDto.ts | 4 + .../models/dtos/Usuarios/UpdatePermisoDto.ts | 5 + .../dtos/Usuarios/UpdateUsuarioRequestDto.ts | 9 + .../src/models/dtos/Usuarios/UsuarioDto.ts | 12 + .../Contables/GestionarTiposPagoPage.tsx | 4 +- .../src/pages/Distribucion/CanillasPage.tsx | 7 - .../pages/Distribucion/DistribuidoresPage.tsx | 7 - .../Distribucion/GestionarCanillitasPage.tsx | 225 ++++++++++++++ .../GestionarDistribuidoresPage.tsx | 196 ++++++++++++ .../Distribucion/GestionarEmpresasPage.tsx | 11 +- .../GestionarOtrosDestinosPage.tsx | 228 ++++++++++++++ .../GestionarPreciosPublicacionPage.tsx | 240 ++++++++++++++ .../GestionarPublicacionesPage.tsx | 260 ++++++++++++++++ .../pages/Distribucion/GestionarZonasPage.tsx | 4 +- .../pages/Distribucion/OtrosDestinosPage.tsx | 7 - .../pages/Distribucion/PublicacionesPage.tsx | 7 - .../Impresion/GestionarEstadosBobinaPage.tsx | 4 +- .../pages/Impresion/GestionarPlantasPage.tsx | 4 +- .../Impresion/GestionarTiposBobinaPage.tsx | 4 +- Frontend/src/pages/LoginPage.tsx | 4 +- .../Usuarios/AsignarPermisosAPerfilPage.tsx | 159 ++++++++++ .../ChangePasswordPagePlaceholder.tsx | 2 +- .../pages/Usuarios/GestionarPerfilesPage.tsx | 237 ++++++++++++++ .../pages/Usuarios/GestionarPermisosPage.tsx | 200 ++++++++++++ .../pages/Usuarios/GestionarUsuariosPage.tsx | 264 ++++++++++++++++ .../src/pages/Usuarios/UsuariosIndexPage.tsx | 68 ++++ Frontend/src/routes/AppRoutes.tsx | 37 ++- .../{ => Contables}/tipoPagoService.ts | 8 +- .../services/Distribucion/canillaService.ts | 46 +++ .../Distribucion/distribuidorService.ts | 41 +++ .../{ => Distribucion}/empresaService.ts | 8 +- .../Distribucion/otroDestinoService.ts | 45 +++ .../services/Distribucion/precioService.ts | 40 +++ .../Distribucion/publicacionService.ts | 46 +++ .../{ => Distribucion}/zonaService.ts | 8 +- .../{ => Impresion}/estadoBobinaService.ts | 8 +- .../services/{ => Impresion}/plantaService.ts | 8 +- .../{ => Impresion}/tipoBobinaService.ts | 8 +- .../services/{ => Usuarios}/authService.ts | 8 +- .../src/services/Usuarios/perfilService.ts | 54 ++++ .../src/services/Usuarios/permisoService.ts | 42 +++ .../src/services/Usuarios/usuarioService.ts | 51 +++ tools/PasswordMigrationUtil/Program.cs | 3 +- .../PasswordMigrationUtil.AssemblyInfo.cs | 2 +- ...wordMigrationUtil.csproj.nuget.dgspec.json | 7 +- ...PasswordMigrationUtil.csproj.nuget.g.props | 6 +- .../obj/project.assets.json | 9 +- 228 files changed, 10745 insertions(+), 178 deletions(-) create mode 100644 Backend/GestionIntegral.Api/Controllers/Distribucion/CanillasController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Distribucion/DistribuidoresController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Distribucion/OtrosDestinosController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Distribucion/PreciosController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Distribucion/PublicacionesController.cs rename Backend/GestionIntegral.Api/Controllers/{ => Usuarios}/AuthController.cs (97%) create mode 100644 Backend/GestionIntegral.Api/Controllers/Usuarios/PerfilesController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Usuarios/PermisosController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Usuarios/UsuariosController.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/CanillaRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ICanillaRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IDistribuidorRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IOtroDestinoRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcMonCanillaRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcPagoRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPrecioRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPubliSeccionRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPublicacionRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IRecargoZonaRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/OtroDestinoRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcMonCanillaRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcPagoRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PrecioRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PubliSeccionRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PublicacionRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/RecargoZonaRepository.cs rename Backend/GestionIntegral.Api/Data/{ => Repositories/Usuarios}/AuthRepository.cs (97%) rename Backend/GestionIntegral.Api/Data/{ => Repositories/Usuarios}/IAuthRepository.cs (78%) create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IPerfilRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IPermisoRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IUsuarioRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Usuarios/PerfilRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Usuarios/PermisoRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Usuarios/UsuarioRepository.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/Canilla.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/CanillaHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/Distribuidor.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/DistribuidorHistorico.cs rename Backend/GestionIntegral.Api/Models/{Empresas => Distribucion}/EmpresaHistorico.cs (100%) create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/OtroDestino.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/OtroDestinoHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/PorcMonCanilla.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/PorcMonCanillaHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/PorcPago.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/PorcPagoHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/Precio.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/PrecioHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/PubliSeccion.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/PubliSeccionHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/Publicacion.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/PublicacionHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/RecargoZona.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/RecargoZonaHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CanillaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateCanillaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateDistribuidorDto.cs rename Backend/GestionIntegral.Api/Models/Dtos/{Empresas => Distribucion}/CreateEmpresaDto.cs (100%) create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateOtroDestinoDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePrecioDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePublicacionDto.cs rename Backend/GestionIntegral.Api/Models/Dtos/{Zonas => Distribucion}/CreateZonaDto.cs (100%) create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/DistribuidorDto.cs rename Backend/GestionIntegral.Api/Models/Dtos/{Empresas => Distribucion}/EmpresaDto.cs (100%) create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/OtroDestinoDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PrecioDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/ToggleBajaCanillaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateCanillaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateDistribuidorDto.cs rename Backend/GestionIntegral.Api/Models/Dtos/{Empresas => Distribucion}/UpdateEmpresaDto.cs (100%) create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateOtroDestinoDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePrecioDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePublicacionDto.cs rename Backend/GestionIntegral.Api/Models/Dtos/{Zonas => Distribucion}/UpdateZonaDto.cs (100%) rename Backend/GestionIntegral.Api/Models/Dtos/{Zonas => Distribucion}/ZonaDto.cs (100%) create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/ActualizarPermisosPerfilRequestDto.cs rename Backend/GestionIntegral.Api/Models/Dtos/{ => Usuarios}/ChangePasswordRequestDto.cs (100%) create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreatePerfilDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreatePermisoDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreateUsuarioRequestDto.cs rename Backend/GestionIntegral.Api/Models/Dtos/{ => Usuarios}/LoginRequestDto.cs (100%) rename Backend/GestionIntegral.Api/Models/Dtos/{ => Usuarios}/LoginResponseDto.cs (100%) create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PerfilDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PermisoAsignadoDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PermisoDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/SetPasswordRequestDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdatePerfilDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdatePermisoDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdateUsuarioRequestDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UsuarioDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Usuarios/Perfil.cs create mode 100644 Backend/GestionIntegral.Api/Models/Usuarios/PerfilHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Usuarios/Permiso.cs create mode 100644 Backend/GestionIntegral.Api/Models/Usuarios/PermisoHistorico.cs rename Backend/GestionIntegral.Api/Models/{ => Usuarios}/Usuario.cs (94%) create mode 100644 Backend/GestionIntegral.Api/Models/Usuarios/UsuarioHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/CanillaService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/DistribuidorService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/ICanillaService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/IDistribuidorService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/IOtroDestinoService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/IPrecioService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/IPublicacionService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/OtroDestinoService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/PrecioService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/PublicacionService.cs rename Backend/GestionIntegral.Api/Services/{ => Usuarios}/AuthService.cs (98%) rename Backend/GestionIntegral.Api/Services/{ => Usuarios}/IAuthService.cs (84%) create mode 100644 Backend/GestionIntegral.Api/Services/Usuarios/IPerfilService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Usuarios/IPermisoService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Usuarios/IUsuarioService.cs rename Backend/GestionIntegral.Api/Services/{ => Usuarios}/PasswordHasherService.cs (97%) create mode 100644 Backend/GestionIntegral.Api/Services/Usuarios/PerfilService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Usuarios/PermisoService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Usuarios/UsuarioService.cs create mode 100644 Backend/GestionIntegral.Api/obj/Debug/net9.0/rbcswa.dswa.cache.json create mode 100644 Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json create mode 100644 Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json create mode 100644 Backend/GestionIntegral.Api/obj/Debug/net9.0/rpswa.dswa.cache.json delete mode 100644 Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.build.GestionIntegral.Api.props delete mode 100644 Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.buildMultiTargeting.GestionIntegral.Api.props delete mode 100644 Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.buildTransitive.GestionIntegral.Api.props rename Frontend/src/components/Modals/{ => Contables}/TipoPagoFormModal.tsx (96%) create mode 100644 Frontend/src/components/Modals/Distribucion/CanillaFormModal.tsx create mode 100644 Frontend/src/components/Modals/Distribucion/DistribuidorFormModal.tsx rename Frontend/src/components/Modals/{ => Distribucion}/EmpresaFormModal.tsx (94%) create mode 100644 Frontend/src/components/Modals/Distribucion/OtroDestinoFormModal.tsx create mode 100644 Frontend/src/components/Modals/Distribucion/PrecioFormModal.tsx create mode 100644 Frontend/src/components/Modals/Distribucion/PublicacionFormModal.tsx rename Frontend/src/components/Modals/{ => Distribucion}/ZonaFormModal.tsx (95%) rename Frontend/src/components/Modals/{ => Impresion}/EstadoBobinaFormModal.tsx (93%) rename Frontend/src/components/Modals/{ => Impresion}/PlantaFormModal.tsx (94%) rename Frontend/src/components/Modals/{ => Impresion}/TipoBobinaFormModal.tsx (92%) rename Frontend/src/components/{ => Modals/Usuarios}/ChangePasswordModal.tsx (96%) create mode 100644 Frontend/src/components/Modals/Usuarios/PerfilFormModal.tsx create mode 100644 Frontend/src/components/Modals/Usuarios/PermisoFormModal.tsx create mode 100644 Frontend/src/components/Modals/Usuarios/PermisosChecklist.tsx create mode 100644 Frontend/src/components/Modals/Usuarios/SetPasswordModal.tsx create mode 100644 Frontend/src/components/Modals/Usuarios/UsuarioFormModal.tsx create mode 100644 Frontend/src/models/dtos/Distribucion/CanillaDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/CreateCanillaDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/CreateDistribuidorDto.ts rename Frontend/src/models/dtos/{Empresas => Distribucion}/CreateEmpresaDto.ts (100%) create mode 100644 Frontend/src/models/dtos/Distribucion/CreateOtroDestinoDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/CreatePrecioDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/CreatePublicacionDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/DistribuidorDto.ts rename Frontend/src/models/dtos/{Empresas => Distribucion}/EmpresaDto.ts (100%) create mode 100644 Frontend/src/models/dtos/Distribucion/OtroDestinoDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/PrecioDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/PublicacionDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/ToggleBajaCanillaDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/UpdateCanillaDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/UpdateDistribuidorDto.ts rename Frontend/src/models/dtos/{Empresas => Distribucion}/UpdateEmpresaDto.ts (100%) create mode 100644 Frontend/src/models/dtos/Distribucion/UpdateOtroDestinoDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/UpdatePrecioDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/UpdatePublicacionDto.ts delete mode 100644 Frontend/src/models/dtos/LoginRequestDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/ActualizarPermisosPerfilRequestDto.ts rename Frontend/src/models/dtos/{ => Usuarios}/ChangePasswordRequestDto.ts (72%) create mode 100644 Frontend/src/models/dtos/Usuarios/CreatePerfilDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/CreatePermisoDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/CreateUsuarioRequestDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/LoginRequestDto.ts rename Frontend/src/models/dtos/{ => Usuarios}/LoginResponseDto.ts (64%) create mode 100644 Frontend/src/models/dtos/Usuarios/PerfilDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/PermisoAsignadoDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/PermisoDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/SetPasswordRequestDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/UpdatePerfilDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/UpdatePermisoDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/UpdateUsuarioRequestDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/UsuarioDto.ts delete mode 100644 Frontend/src/pages/Distribucion/CanillasPage.tsx delete mode 100644 Frontend/src/pages/Distribucion/DistribuidoresPage.tsx create mode 100644 Frontend/src/pages/Distribucion/GestionarCanillitasPage.tsx create mode 100644 Frontend/src/pages/Distribucion/GestionarDistribuidoresPage.tsx create mode 100644 Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx create mode 100644 Frontend/src/pages/Distribucion/GestionarPreciosPublicacionPage.tsx create mode 100644 Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx delete mode 100644 Frontend/src/pages/Distribucion/OtrosDestinosPage.tsx delete mode 100644 Frontend/src/pages/Distribucion/PublicacionesPage.tsx create mode 100644 Frontend/src/pages/Usuarios/AsignarPermisosAPerfilPage.tsx rename Frontend/src/pages/{ => Usuarios}/ChangePasswordPagePlaceholder.tsx (92%) create mode 100644 Frontend/src/pages/Usuarios/GestionarPerfilesPage.tsx create mode 100644 Frontend/src/pages/Usuarios/GestionarPermisosPage.tsx create mode 100644 Frontend/src/pages/Usuarios/GestionarUsuariosPage.tsx create mode 100644 Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx rename Frontend/src/services/{ => Contables}/tipoPagoService.ts (81%) create mode 100644 Frontend/src/services/Distribucion/canillaService.ts create mode 100644 Frontend/src/services/Distribucion/distribuidorService.ts rename Frontend/src/services/{ => Distribucion}/empresaService.ts (83%) create mode 100644 Frontend/src/services/Distribucion/otroDestinoService.ts create mode 100644 Frontend/src/services/Distribucion/precioService.ts create mode 100644 Frontend/src/services/Distribucion/publicacionService.ts rename Frontend/src/services/{ => Distribucion}/zonaService.ts (84%) rename Frontend/src/services/{ => Impresion}/estadoBobinaService.ts (79%) rename Frontend/src/services/{ => Impresion}/plantaService.ts (83%) rename Frontend/src/services/{ => Impresion}/tipoBobinaService.ts (82%) rename Frontend/src/services/{ => Usuarios}/authService.ts (61%) create mode 100644 Frontend/src/services/Usuarios/perfilService.ts create mode 100644 Frontend/src/services/Usuarios/permisoService.ts create mode 100644 Frontend/src/services/Usuarios/usuarioService.ts diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/CanillasController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/CanillasController.cs new file mode 100644 index 0000000..b233884 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/CanillasController.cs @@ -0,0 +1,133 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/[controller]")] + [ApiController] + [Authorize] + public class CanillasController : ControllerBase + { + private readonly ICanillaService _canillaService; + private readonly ILogger _logger; + + // Permisos para Canillas (CG001 a CG005) + private const string PermisoVer = "CG001"; + private const string PermisoCrear = "CG002"; + private const string PermisoModificar = "CG003"; + // CG004 es para Porcentajes, se manejará en un endpoint específico o en Update si se incluye en DTO + private const string PermisoBaja = "CG005"; // Para dar de baja/alta + + public CanillasController(ICanillaService canillaService, ILogger logger) + { + _canillaService = canillaService; + _logger = logger; + } + + private bool TienePermiso(string codAccRequerido) + { + if (User.IsInRole("SuperAdmin")) return true; + return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido); + } + + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) return userId; + return null; + } + + // GET: api/canillas + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAllCanillas([FromQuery] string? nomApe, [FromQuery] int? legajo, [FromQuery] bool? soloActivos = true) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var canillas = await _canillaService.ObtenerTodosAsync(nomApe, legajo, soloActivos); + return Ok(canillas); + } + + // GET: api/canillas/{id} + [HttpGet("{id:int}", Name = "GetCanillaById")] + [ProducesResponseType(typeof(CanillaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetCanillaById(int id) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var canilla = await _canillaService.ObtenerPorIdAsync(id); + if (canilla == null) return NotFound(); + return Ok(canilla); + } + + // POST: api/canillas + [HttpPost] + [ProducesResponseType(typeof(CanillaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreateCanilla([FromBody] CreateCanillaDto createDto) + { + if (!TienePermiso(PermisoCrear)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized(); + + var (canillaCreado, error) = await _canillaService.CrearAsync(createDto, idUsuario.Value); + if (error != null) return BadRequest(new { message = error }); + if (canillaCreado == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear el canillita."); + + return CreatedAtRoute("GetCanillaById", new { id = canillaCreado.IdCanilla }, canillaCreado); + } + + // PUT: api/canillas/{id} + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateCanilla(int id, [FromBody] UpdateCanillaDto updateDto) + { + if (!TienePermiso(PermisoModificar)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized(); + + var (exito, error) = await _canillaService.ActualizarAsync(id, updateDto, idUsuario.Value); + if (!exito) + { + if (error == "Canillita no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // POST: api/canillas/{id}/toggle-baja + [HttpPost("{id:int}/toggle-baja")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task ToggleBajaCanilla(int id, [FromBody] ToggleBajaCanillaDto bajaDto) + { + if (!TienePermiso(PermisoBaja)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized(); + + var (exito, error) = await _canillaService.ToggleBajaAsync(id, bajaDto.DarDeBaja, idUsuario.Value); + if (!exito) + { + if (error == "Canillita no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/DistribuidoresController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/DistribuidoresController.cs new file mode 100644 index 0000000..910684e --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/DistribuidoresController.cs @@ -0,0 +1,120 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/[controller]")] + [ApiController] + [Authorize] + public class DistribuidoresController : ControllerBase + { + private readonly IDistribuidorService _distribuidorService; + private readonly ILogger _logger; + + // Permisos para Distribuidores (DG001 a DG005) + private const string PermisoVer = "DG001"; + private const string PermisoCrear = "DG002"; + private const string PermisoModificar = "DG003"; + // DG004 es para Porcentajes, se manejará en un endpoint/controlador dedicado + private const string PermisoEliminar = "DG005"; + + public DistribuidoresController(IDistribuidorService distribuidorService, ILogger logger) + { + _distribuidorService = distribuidorService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + return null; + } + + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAllDistribuidores([FromQuery] string? nombre, [FromQuery] string? nroDoc) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var distribuidores = await _distribuidorService.ObtenerTodosAsync(nombre, nroDoc); + return Ok(distribuidores); + } + + [HttpGet("{id:int}", Name = "GetDistribuidorById")] + [ProducesResponseType(typeof(DistribuidorDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetDistribuidorById(int id) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var distribuidor = await _distribuidorService.ObtenerPorIdAsync(id); + if (distribuidor == null) return NotFound(); + return Ok(distribuidor); + } + + [HttpPost] + [ProducesResponseType(typeof(DistribuidorDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreateDistribuidor([FromBody] CreateDistribuidorDto createDto) + { + if (!TienePermiso(PermisoCrear)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _distribuidorService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear."); + return CreatedAtRoute("GetDistribuidorById", new { id = dto.IdDistribuidor }, dto); + } + + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateDistribuidor(int id, [FromBody] UpdateDistribuidorDto updateDto) + { + if (!TienePermiso(PermisoModificar)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _distribuidorService.ActualizarAsync(id, updateDto, userId.Value); + if (!exito) + { + if (error == "Distribuidor no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteDistribuidor(int id) + { + if (!TienePermiso(PermisoEliminar)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _distribuidorService.EliminarAsync(id, userId.Value); + if (!exito) + { + if (error == "Distribuidor no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/OtrosDestinosController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/OtrosDestinosController.cs new file mode 100644 index 0000000..2c97f7d --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/OtrosDestinosController.cs @@ -0,0 +1,180 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/[controller]")] // Ruta base: /api/otrosdestinos + [ApiController] + [Authorize] + public class OtrosDestinosController : ControllerBase + { + private readonly IOtroDestinoService _otroDestinoService; + private readonly ILogger _logger; + + // Permisos para Otros Destinos (basado en el Excel OD001-OD004) + private const string PermisoVer = "OD001"; // Aunque tu Excel dice "Salidas Otros Destinos", lo asumo para la entidad Otros Destinos + private const string PermisoCrear = "OD002"; // Asumo para la entidad + private const string PermisoModificar = "OD003"; // Asumo para la entidad + private const string PermisoEliminar = "OD004"; // Asumo para la entidad + + public OtrosDestinosController(IOtroDestinoService otroDestinoService, ILogger logger) + { + _otroDestinoService = otroDestinoService ?? throw new ArgumentNullException(nameof(otroDestinoService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + private bool TienePermiso(string codAccRequerido) + { + if (User.IsInRole("SuperAdmin")) return true; + return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido); + } + + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en OtrosDestinosController."); + return null; + } + + // GET: api/otrosdestinos + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetAllOtrosDestinos([FromQuery] string? nombre) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + try + { + var destinos = await _otroDestinoService.ObtenerTodosAsync(nombre); + return Ok(destinos); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Otros Destinos. Filtro: {Nombre}", nombre); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // GET: api/otrosdestinos/{id} + [HttpGet("{id:int}", Name = "GetOtroDestinoById")] + [ProducesResponseType(typeof(OtroDestinoDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetOtroDestinoById(int id) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + try + { + var destino = await _otroDestinoService.ObtenerPorIdAsync(id); + if (destino == null) return NotFound(new { message = $"Otro Destino con ID {id} no encontrado." }); + return Ok(destino); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Otro Destino por ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // POST: api/otrosdestinos + [HttpPost] + [ProducesResponseType(typeof(OtroDestinoDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreateOtroDestino([FromBody] CreateOtroDestinoDto createDto) + { + if (!TienePermiso(PermisoCrear)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (destinoCreado, error) = await _otroDestinoService.CrearAsync(createDto, idUsuario.Value); + if (error != null) return BadRequest(new { message = error }); + if (destinoCreado == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear."); + return CreatedAtRoute("GetOtroDestinoById", new { id = destinoCreado.IdDestino }, destinoCreado); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear Otro Destino. Nombre: {Nombre}, UserID: {UsuarioId}", createDto.Nombre, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // PUT: api/otrosdestinos/{id} + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task UpdateOtroDestino(int id, [FromBody] UpdateOtroDestinoDto updateDto) + { + if (!TienePermiso(PermisoModificar)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (exito, error) = await _otroDestinoService.ActualizarAsync(id, updateDto, idUsuario.Value); + if (!exito) + { + if (error == "Otro Destino no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar Otro Destino ID: {Id}, UserID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // DELETE: api/otrosdestinos/{id} + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task DeleteOtroDestino(int id) + { + if (!TienePermiso(PermisoEliminar)) return Forbid(); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (exito, error) = await _otroDestinoService.EliminarAsync(id, idUsuario.Value); + if (!exito) + { + if (error == "Otro Destino no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar Otro Destino ID: {Id}, UserID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/PreciosController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/PreciosController.cs new file mode 100644 index 0000000..aba1a90 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/PreciosController.cs @@ -0,0 +1,144 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/publicaciones/{idPublicacion}/precios")] // Anidado bajo publicaciones + [ApiController] + [Authorize] + public class PreciosController : ControllerBase + { + private readonly IPrecioService _precioService; + private readonly ILogger _logger; + + // Permiso DP004 para gestionar precios + private const string PermisoGestionarPrecios = "DP004"; + + public PreciosController(IPrecioService precioService, ILogger logger) + { + _precioService = precioService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + return null; + } + + // GET: api/publicaciones/{idPublicacion}/precios + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetPreciosPorPublicacion(int idPublicacion) + { + // Para ver precios, se podría usar el permiso de ver publicaciones (DP001) o el de gestionar precios (DP004) + if (!TienePermiso("DP001") && !TienePermiso(PermisoGestionarPrecios)) return Forbid(); + + var precios = await _precioService.ObtenerPorPublicacionIdAsync(idPublicacion); + return Ok(precios); + } + + // GET: api/publicaciones/{idPublicacion}/precios/{idPrecio} + // Este endpoint es más para obtener un precio específico para editarlo, no tanto para el listado. + // El anterior es más útil para mostrar todos los periodos de una publicación. + [HttpGet("{idPrecio:int}", Name = "GetPrecioById")] + [ProducesResponseType(typeof(PrecioDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetPrecioById(int idPublicacion, int idPrecio) + { + if (!TienePermiso("DP001") && !TienePermiso(PermisoGestionarPrecios)) return Forbid(); + var precio = await _precioService.ObtenerPorIdAsync(idPrecio); + if (precio == null || precio.IdPublicacion != idPublicacion) return NotFound(); // Asegurar que el precio pertenece a la publicación + return Ok(precio); + } + + + // POST: api/publicaciones/{idPublicacion}/precios + [HttpPost] + [ProducesResponseType(typeof(PrecioDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreatePrecio(int idPublicacion, [FromBody] CreatePrecioDto createDto) + { + if (!TienePermiso(PermisoGestionarPrecios)) return Forbid(); + if (idPublicacion != createDto.IdPublicacion) + return BadRequest(new { message = "El ID de publicación en la ruta no coincide con el del cuerpo de la solicitud." }); + if (!ModelState.IsValid) return BadRequest(ModelState); + + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _precioService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear el período de precio."); + + // La ruta para "GetPrecioById" necesita ambos IDs + return CreatedAtRoute("GetPrecioById", new { idPublicacion = dto.IdPublicacion, idPrecio = dto.IdPrecio }, dto); + } + + // PUT: api/publicaciones/{idPublicacion}/precios/{idPrecio} + [HttpPut("{idPrecio:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdatePrecio(int idPublicacion, int idPrecio, [FromBody] UpdatePrecioDto updateDto) + { + if (!TienePermiso(PermisoGestionarPrecios)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + // Verificar que el precio que se intenta actualizar pertenece a la publicación de la ruta + var precioExistente = await _precioService.ObtenerPorIdAsync(idPrecio); + if (precioExistente == null || precioExistente.IdPublicacion != idPublicacion) + { + return NotFound(new { message = "Período de precio no encontrado para esta publicación."}); + } + + var (exito, error) = await _precioService.ActualizarAsync(idPrecio, updateDto, userId.Value); + if (!exito) + { + // El servicio ya devuelve "Período de precio no encontrado." si es el caso + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // DELETE: api/publicaciones/{idPublicacion}/precios/{idPrecio} + [HttpDelete("{idPrecio:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeletePrecio(int idPublicacion, int idPrecio) + { + if (!TienePermiso(PermisoGestionarPrecios)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var precioExistente = await _precioService.ObtenerPorIdAsync(idPrecio); + if (precioExistente == null || precioExistente.IdPublicacion != idPublicacion) + { + return NotFound(new { message = "Período de precio no encontrado para esta publicación."}); + } + + var (exito, error) = await _precioService.EliminarAsync(idPrecio, userId.Value); + if (!exito) + { + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/PublicacionesController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/PublicacionesController.cs new file mode 100644 index 0000000..6195731 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/PublicacionesController.cs @@ -0,0 +1,121 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/[controller]")] + [ApiController] + [Authorize] + public class PublicacionesController : ControllerBase + { + private readonly IPublicacionService _publicacionService; + private readonly ILogger _logger; + + // Permisos (DP001 a DP006) + private const string PermisoVer = "DP001"; + private const string PermisoCrear = "DP002"; + private const string PermisoModificar = "DP003"; + // DP004 (Precios) y DP005 (Recargos) se manejarán en endpoints/controladores dedicados + private const string PermisoEliminar = "DP006"; + // DP007 (Secciones) también se manejará por separado + + public PublicacionesController(IPublicacionService publicacionService, ILogger logger) + { + _publicacionService = publicacionService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + return null; + } + + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAllPublicaciones([FromQuery] string? nombre, [FromQuery] int? idEmpresa, [FromQuery] bool? soloHabilitadas = true) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var publicaciones = await _publicacionService.ObtenerTodasAsync(nombre, idEmpresa, soloHabilitadas); + return Ok(publicaciones); + } + + [HttpGet("{id:int}", Name = "GetPublicacionById")] + [ProducesResponseType(typeof(PublicacionDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetPublicacionById(int id) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var publicacion = await _publicacionService.ObtenerPorIdAsync(id); + if (publicacion == null) return NotFound(); + return Ok(publicacion); + } + + [HttpPost] + [ProducesResponseType(typeof(PublicacionDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreatePublicacion([FromBody] CreatePublicacionDto createDto) + { + if (!TienePermiso(PermisoCrear)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _publicacionService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear."); + return CreatedAtRoute("GetPublicacionById", new { id = dto.IdPublicacion }, dto); + } + + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdatePublicacion(int id, [FromBody] UpdatePublicacionDto updateDto) + { + if (!TienePermiso(PermisoModificar)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _publicacionService.ActualizarAsync(id, updateDto, userId.Value); + if (!exito) + { + if (error == "Publicación no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeletePublicacion(int id) + { + if (!TienePermiso(PermisoEliminar)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _publicacionService.EliminarAsync(id, userId.Value); + if (!exito) + { + if (error == "Publicación no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/AuthController.cs b/Backend/GestionIntegral.Api/Controllers/Usuarios/AuthController.cs similarity index 97% rename from Backend/GestionIntegral.Api/Controllers/AuthController.cs rename to Backend/GestionIntegral.Api/Controllers/Usuarios/AuthController.cs index 30f0d5a..173b280 100644 --- a/Backend/GestionIntegral.Api/Controllers/AuthController.cs +++ b/Backend/GestionIntegral.Api/Controllers/Usuarios/AuthController.cs @@ -1,10 +1,10 @@ using GestionIntegral.Api.Dtos; -using GestionIntegral.Api.Services; +using GestionIntegral.Api.Services.Usuarios; using Microsoft.AspNetCore.Authorization; // Para [Authorize] using Microsoft.AspNetCore.Mvc; using System.Security.Claims; // Para leer claims del token -namespace GestionIntegral.Api.Controllers +namespace GestionIntegral.Api.Controllers.Usuarios { [Route("api/[controller]")] // Ruta base: /api/auth [ApiController] diff --git a/Backend/GestionIntegral.Api/Controllers/Usuarios/PerfilesController.cs b/Backend/GestionIntegral.Api/Controllers/Usuarios/PerfilesController.cs new file mode 100644 index 0000000..cebd4ab --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Usuarios/PerfilesController.cs @@ -0,0 +1,239 @@ +using GestionIntegral.Api.Dtos.Usuarios; +using GestionIntegral.Api.Services.Usuarios; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Usuarios +{ + [Route("api/[controller]")] // Ruta base: /api/perfiles + [ApiController] + [Authorize] + public class PerfilesController : ControllerBase + { + private readonly IPerfilService _perfilService; + private readonly ILogger _logger; + + // Códigos de permiso para Perfiles (PU001-PU004) + private const string PermisoVer = "PU001"; + private const string PermisoCrear = "PU002"; + private const string PermisoModificar = "PU003"; + private const string PermisoEliminar = "PU003"; + private const string PermisoAsignar = "PU004"; + + public PerfilesController(IPerfilService perfilService, ILogger logger) + { + _perfilService = perfilService ?? throw new ArgumentNullException(nameof(perfilService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + private bool TienePermiso(string codAccRequerido) + { + if (User.IsInRole("SuperAdmin")) return true; + return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido); + } + + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en PerfilesController."); + return null; + } + + // GET: api/perfiles + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAllPerfiles([FromQuery] string? nombre) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + try + { + var perfiles = await _perfilService.ObtenerTodosAsync(nombre); + return Ok(perfiles); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Perfiles. Filtro: {Nombre}", nombre); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // GET: api/perfiles/{id} + [HttpGet("{id:int}", Name = "GetPerfilById")] + [ProducesResponseType(typeof(PerfilDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetPerfilById(int id) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + try + { + var perfil = await _perfilService.ObtenerPorIdAsync(id); + if (perfil == null) return NotFound(new { message = $"Perfil con ID {id} no encontrado." }); + return Ok(perfil); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Perfil por ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // POST: api/perfiles + [HttpPost] + [ProducesResponseType(typeof(PerfilDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreatePerfil([FromBody] CreatePerfilDto createDto) + { + if (!TienePermiso(PermisoCrear)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId(); // Necesario para auditoría si se implementa + if (idUsuario == null) return Unauthorized("Token inválido."); + + + try + { + var (perfilCreado, error) = await _perfilService.CrearAsync(createDto, idUsuario.Value); + if (error != null) return BadRequest(new { message = error }); + if (perfilCreado == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear el perfil."); + return CreatedAtRoute("GetPerfilById", new { id = perfilCreado.Id }, perfilCreado); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear Perfil. Nombre: {Nombre}, UserID: {UsuarioId}", createDto.NombrePerfil, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // PUT: api/perfiles/{id} + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdatePerfil(int id, [FromBody] UpdatePerfilDto updateDto) + { + // Asumo que PU003 es para modificar el perfil (nombre/descripción) + if (!TienePermiso(PermisoModificar)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (exito, error) = await _perfilService.ActualizarAsync(id, updateDto, idUsuario.Value); + if (!exito) + { + if (error == "Perfil no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar Perfil ID: {Id}, UserID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // DELETE: api/perfiles/{id} + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeletePerfil(int id) + { + // El Excel dice PU003 para eliminar. + if (!TienePermiso(PermisoEliminar)) return Forbid(); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (exito, error) = await _perfilService.EliminarAsync(id, idUsuario.Value); + if (!exito) + { + if (error == "Perfil no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar Perfil ID: {Id}, UserID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + // GET: api/perfiles/{idPerfil}/permisos + [HttpGet("{idPerfil:int}/permisos")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] // Si no tiene permiso PU001 o PU004 + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetPermisosPorPerfil(int idPerfil) + { + // Para ver los permisos de un perfil, podría ser PU001 (ver perfil) o PU004 (asignar) + if (!TienePermiso(PermisoVer) && !TienePermiso(PermisoAsignar)) return Forbid(); + + try + { + var perfil = await _perfilService.ObtenerPorIdAsync(idPerfil); // Verificar si el perfil existe + if (perfil == null) + { + return NotFound(new { message = $"Perfil con ID {idPerfil} no encontrado." }); + } + + var permisos = await _perfilService.ObtenerPermisosAsignadosAsync(idPerfil); + return Ok(permisos); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener permisos para el Perfil ID: {IdPerfil}", idPerfil); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener los permisos del perfil."); + } + } + + // PUT: api/perfiles/{idPerfil}/permisos + [HttpPut("{idPerfil:int}/permisos")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] // Si no tiene permiso PU004 + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task UpdatePermisosDelPerfil(int idPerfil, [FromBody] ActualizarPermisosPerfilRequestDto request) + { + if (!TienePermiso(PermisoAsignar)) return Forbid(); + + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (exito, error) = await _perfilService.ActualizarPermisosAsignadosAsync(idPerfil, request, idUsuario.Value); + if (!exito) + { + if (error == "Perfil no encontrado.") return NotFound(new { message = error }); + // Otros errores como "permiso inválido" + return BadRequest(new { message = error }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar permisos para Perfil ID: {IdPerfil} por Usuario ID: {UsuarioId}", idPerfil, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al actualizar los permisos del perfil."); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Usuarios/PermisosController.cs b/Backend/GestionIntegral.Api/Controllers/Usuarios/PermisosController.cs new file mode 100644 index 0000000..f5b7873 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Usuarios/PermisosController.cs @@ -0,0 +1,153 @@ +using GestionIntegral.Api.Dtos.Usuarios; +using GestionIntegral.Api.Services.Usuarios; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Usuarios +{ + [Route("api/[controller]")] // Ruta base: /api/permisos + [ApiController] + [Authorize(Roles = "SuperAdmin")] // Solo SuperAdmin puede gestionar la definición de permisos + public class PermisosController : ControllerBase + { + private readonly IPermisoService _permisoService; + private readonly ILogger _logger; + + public PermisosController(IPermisoService permisoService, ILogger logger) + { + _permisoService = permisoService ?? throw new ArgumentNullException(nameof(permisoService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + // Helper para User ID (aunque en SuperAdmin puede no ser tan crítico para auditoría de *definición* de permisos) + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en PermisosController."); + return null; + } + + // GET: api/permisos + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetAllPermisos([FromQuery] string? modulo, [FromQuery] string? codAcc) + { + try + { + var permisos = await _permisoService.ObtenerTodosAsync(modulo, codAcc); + return Ok(permisos); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Permisos. Filtros: Modulo={Modulo}, CodAcc={CodAcc}", modulo, codAcc); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // GET: api/permisos/{id} + [HttpGet("{id:int}", Name = "GetPermisoById")] + [ProducesResponseType(typeof(PermisoDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetPermisoById(int id) + { + try + { + var permiso = await _permisoService.ObtenerPorIdAsync(id); + if (permiso == null) return NotFound(new { message = $"Permiso con ID {id} no encontrado." }); + return Ok(permiso); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Permiso por ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // POST: api/permisos + [HttpPost] + [ProducesResponseType(typeof(PermisoDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreatePermiso([FromBody] CreatePermisoDto createDto) + { + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId() ?? 0; // SuperAdmin puede no necesitar auditoría estricta aquí + + try + { + var (permisoCreado, error) = await _permisoService.CrearAsync(createDto, idUsuario); + if (error != null) return BadRequest(new { message = error }); + if (permisoCreado == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear el permiso."); + return CreatedAtRoute("GetPermisoById", new { id = permisoCreado.Id }, permisoCreado); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear Permiso. CodAcc: {CodAcc}, UserID: {UsuarioId}", createDto.CodAcc, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // PUT: api/permisos/{id} + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task UpdatePermiso(int id, [FromBody] UpdatePermisoDto updateDto) + { + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId() ?? 0; + + try + { + var (exito, error) = await _permisoService.ActualizarAsync(id, updateDto, idUsuario); + if (!exito) + { + if (error == "Permiso no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar Permiso ID: {Id}, UserID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // DELETE: api/permisos/{id} + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task DeletePermiso(int id) + { + var idUsuario = GetCurrentUserId() ?? 0; + + try + { + var (exito, error) = await _permisoService.EliminarAsync(id, idUsuario); + if (!exito) + { + if (error == "Permiso no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); // Ej: "En uso" + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar Permiso ID: {Id}, UserID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Usuarios/UsuariosController.cs b/Backend/GestionIntegral.Api/Controllers/Usuarios/UsuariosController.cs new file mode 100644 index 0000000..4ef0808 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Usuarios/UsuariosController.cs @@ -0,0 +1,158 @@ +using GestionIntegral.Api.Dtos.Usuarios; +using GestionIntegral.Api.Services.Usuarios; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Usuarios +{ + [Route("api/[controller]")] + [ApiController] + [Authorize] // Requiere autenticación general + public class UsuariosController : ControllerBase + { + private readonly IUsuarioService _usuarioService; + private readonly ILogger _logger; + + // Permisos para gestión de usuarios (CU001-CU004) + private const string PermisoVerUsuarios = "CU001"; + private const string PermisoAgregarUsuarios = "CU002"; + private const string PermisoModificarUsuarios = "CU003"; // Asumo que CU003 es para modificar, aunque el Excel dice "Eliminar" + private const string PermisoAsignarPerfil = "CU004"; // Este se relaciona con la edición del perfil del usuario + + public UsuariosController(IUsuarioService usuarioService, ILogger logger) + { + _usuarioService = usuarioService; + _logger = logger; + } + + private bool TienePermiso(string codAccRequerido) + { + if (User.IsInRole("SuperAdmin")) return true; + return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido); + } + + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en UsuariosController."); + return null; + } + + // GET: api/usuarios + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAllUsuarios([FromQuery] string? user, [FromQuery] string? nombre) + { + if (!TienePermiso(PermisoVerUsuarios)) return Forbid(); + var usuarios = await _usuarioService.ObtenerTodosAsync(user, nombre); + return Ok(usuarios); + } + + // GET: api/usuarios/{id} + [HttpGet("{id:int}", Name = "GetUsuarioById")] + [ProducesResponseType(typeof(UsuarioDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetUsuarioById(int id) + { + if (!TienePermiso(PermisoVerUsuarios)) return Forbid(); + var usuario = await _usuarioService.ObtenerPorIdAsync(id); + if (usuario == null) return NotFound(); + return Ok(usuario); + } + + // POST: api/usuarios + [HttpPost] + [ProducesResponseType(typeof(UsuarioDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreateUsuario([FromBody] CreateUsuarioRequestDto createDto) + { + if (!TienePermiso(PermisoAgregarUsuarios)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuarioCreador = GetCurrentUserId(); + if (idUsuarioCreador == null) return Unauthorized("Token inválido."); + + var (usuarioCreado, error) = await _usuarioService.CrearAsync(createDto, idUsuarioCreador.Value); + if (error != null) return BadRequest(new { message = error }); + if (usuarioCreado == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear el usuario."); + + return CreatedAtRoute("GetUsuarioById", new { id = usuarioCreado.Id }, usuarioCreado); + } + + // PUT: api/usuarios/{id} + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] // CU003 o CU004 + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateUsuario(int id, [FromBody] UpdateUsuarioRequestDto updateDto) + { + // Modificar datos básicos (CU003), reasignar perfil (CU004) + if (!TienePermiso(PermisoModificarUsuarios) && !TienePermiso(PermisoAsignarPerfil)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuarioModificador = GetCurrentUserId(); + if (idUsuarioModificador == null) return Unauthorized("Token inválido."); + + var (exito, error) = await _usuarioService.ActualizarAsync(id, updateDto, idUsuarioModificador.Value); + if (!exito) + { + if (error == "Usuario no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // POST: api/usuarios/{id}/set-password + [HttpPost("{id:int}/set-password")] + [Authorize(Roles = "SuperAdmin")] // Solo SuperAdmin puede resetear claves directamente + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task SetPassword(int id, [FromBody] SetPasswordRequestDto setPasswordDto) + { + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idAdmin = GetCurrentUserId(); + if (idAdmin == null) return Unauthorized("Token inválido."); + + var (exito, error) = await _usuarioService.SetPasswordAsync(id, setPasswordDto, idAdmin.Value); + if (!exito) + { + if (error == "Usuario no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // POST: api/usuarios/{id}/toggle-habilitado + [HttpPost("{id:int}/toggle-habilitado")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] // Podría ser PermisoModificarUsuarios (CU003) + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task ToggleHabilitado(int id, [FromBody] bool habilitar) + { + if (!TienePermiso(PermisoModificarUsuarios)) return Forbid(); + + var idAdmin = GetCurrentUserId(); + if (idAdmin == null) return Unauthorized("Token inválido."); + + var (exito, error) = await _usuarioService.CambiarEstadoHabilitadoAsync(id, habilitar, idAdmin.Value); + if (!exito) + { + if (error == "Usuario no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/CanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/CanillaRepository.cs new file mode 100644 index 0000000..61a85d5 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/CanillaRepository.cs @@ -0,0 +1,205 @@ +using Dapper; +using GestionIntegral.Api.Data.Repositories; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class CanillaRepository : ICanillaRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public CanillaRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos) + { + var sqlBuilder = new StringBuilder(@" + SELECT c.*, z.Nombre AS NombreZona, ISNULL(e.Nombre, 'N/A (Accionista)') AS NombreEmpresa + FROM dbo.dist_dtCanillas c + INNER JOIN dbo.dist_dtZonas z ON c.Id_Zona = z.Id_Zona + LEFT JOIN dbo.dist_dtEmpresas e ON c.Empresa = e.Id_Empresa -- Empresa 0 no tendrá join + WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (soloActivos.HasValue) + { + sqlBuilder.Append(soloActivos.Value ? " AND c.Baja = 0" : " AND c.Baja = 1"); + } + if (!string.IsNullOrWhiteSpace(nomApeFilter)) + { + sqlBuilder.Append(" AND c.NomApe LIKE @NomApe"); + parameters.Add("NomApe", $"%{nomApeFilter}%"); + } + if (legajoFilter.HasValue) + { + sqlBuilder.Append(" AND c.Legajo = @Legajo"); + parameters.Add("Legajo", legajoFilter.Value); + } + sqlBuilder.Append(" ORDER BY c.NomApe;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync( + sqlBuilder.ToString(), + (canilla, nombreZona, nombreEmpresa) => (canilla, nombreZona, nombreEmpresa), + parameters, + splitOn: "NombreZona,NombreEmpresa" + ); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Canillas."); + return Enumerable.Empty<(Canilla, string, string)>(); + } + } + + public async Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id) + { + const string sql = @" + SELECT c.*, z.Nombre AS NombreZona, ISNULL(e.Nombre, 'N/A (Accionista)') AS NombreEmpresa + FROM dbo.dist_dtCanillas c + INNER JOIN dbo.dist_dtZonas z ON c.Id_Zona = z.Id_Zona + LEFT JOIN dbo.dist_dtEmpresas e ON c.Empresa = e.Id_Empresa + WHERE c.Id_Canilla = @Id"; + try + { + using var connection = _connectionFactory.CreateConnection(); + var result = await connection.QueryAsync( + sql, + (canilla, nombreZona, nombreEmpresa) => (canilla, nombreZona, nombreEmpresa), + new { Id = id }, + splitOn: "NombreZona,NombreEmpresa" + ); + return result.SingleOrDefault(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Canilla por ID: {IdCanilla}", id); + return (null, null, null); + } + } + public async Task GetByIdSimpleAsync(int id) // Para uso interno del servicio + { + const string sql = "SELECT * FROM dbo.dist_dtCanillas WHERE Id_Canilla = @Id"; + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + + + public async Task ExistsByLegajoAsync(int legajo, int? excludeIdCanilla = null) + { + if (legajo == 0) return false; // Legajo 0 es como nulo, no debería validarse como único + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtCanillas WHERE Legajo = @Legajo AND Legajo != 0"); // Excluir legajo 0 de la unicidad + var parameters = new DynamicParameters(); + parameters.Add("Legajo", legajo); + + if (excludeIdCanilla.HasValue) + { + sqlBuilder.Append(" AND Id_Canilla != @ExcludeIdCanilla"); + parameters.Add("ExcludeIdCanilla", excludeIdCanilla.Value); + } + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByLegajoAsync para Canilla con Legajo: {Legajo}", legajo); + return true; + } + } + + public async Task CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_dtCanillas (Legajo, NomApe, Parada, Id_Zona, Accionista, Obs, Empresa, Baja, FechaBaja) + OUTPUT INSERTED.* + VALUES (@Legajo, @NomApe, @Parada, @IdZona, @Accionista, @Obs, @Empresa, @Baja, @FechaBaja);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtCanillas_H + (Id_Canilla, Legajo, NomApe, Parada, Id_Zona, Accionista, Obs, Empresa, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdCanilla, @Legajo, @NomApe, @Parada, @IdZona, @Accionista, @Obs, @Empresa, @Baja, @FechaBaja, @Id_Usuario, @FechaMod, @TipoMod);"; + + var connection = transaction.Connection!; + var insertedCanilla = await connection.QuerySingleAsync(sqlInsert, nuevoCanilla, transaction); + + if (insertedCanilla == null) throw new DataException("No se pudo crear el canilla."); + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + insertedCanilla.IdCanilla, insertedCanilla.Legajo, insertedCanilla.NomApe, insertedCanilla.Parada, insertedCanilla.IdZona, + insertedCanilla.Accionista, insertedCanilla.Obs, insertedCanilla.Empresa, insertedCanilla.Baja, insertedCanilla.FechaBaja, + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Creado" + }, transaction); + return insertedCanilla; + } + + public async Task UpdateAsync(Canilla canillaAActualizar, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var canillaActual = await connection.QuerySingleOrDefaultAsync( + "SELECT * FROM dbo.dist_dtCanillas WHERE Id_Canilla = @IdCanilla", new { canillaAActualizar.IdCanilla }, transaction); + if (canillaActual == null) throw new KeyNotFoundException("Canilla no encontrado para actualizar."); + + const string sqlUpdate = @" + UPDATE dbo.dist_dtCanillas SET + Legajo = @Legajo, NomApe = @NomApe, Parada = @Parada, Id_Zona = @IdZona, + Accionista = @Accionista, Obs = @Obs, Empresa = @Empresa + -- Baja y FechaBaja se manejan por ToggleBajaAsync + WHERE Id_Canilla = @IdCanilla;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtCanillas_H + (Id_Canilla, Legajo, NomApe, Parada, Id_Zona, Accionista, Obs, Empresa, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdCanilla, @LegajoActual, @NomApeActual, @ParadaActual, @IdZonaActual, @AccionistaActual, @ObsActual, @EmpresaActual, @BajaActual, @FechaBajaActual, @Id_Usuario, @FechaMod, @TipoMod);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdCanilla = canillaActual.IdCanilla, + LegajoActual = canillaActual.Legajo, NomApeActual = canillaActual.NomApe, ParadaActual = canillaActual.Parada, IdZonaActual = canillaActual.IdZona, + AccionistaActual = canillaActual.Accionista, ObsActual = canillaActual.Obs, EmpresaActual = canillaActual.Empresa, + BajaActual = canillaActual.Baja, FechaBajaActual = canillaActual.FechaBaja, // Registrar estado actual de baja + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Actualizado" + }, transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, canillaAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var canillaActual = await connection.QuerySingleOrDefaultAsync( + "SELECT * FROM dbo.dist_dtCanillas WHERE Id_Canilla = @IdCanilla", new { IdCanilla = id }, transaction); + if (canillaActual == null) throw new KeyNotFoundException("Canilla no encontrado para dar de baja/alta."); + + const string sqlUpdate = "UPDATE dbo.dist_dtCanillas SET Baja = @Baja, FechaBaja = @FechaBaja WHERE Id_Canilla = @IdCanilla;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtCanillas_H + (Id_Canilla, Legajo, NomApe, Parada, Id_Zona, Accionista, Obs, Empresa, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdCanilla, @Legajo, @NomApe, @Parada, @IdZona, @Accionista, @Obs, @Empresa, @BajaNueva, @FechaBajaNueva, @Id_Usuario, @FechaMod, @TipoModHist);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + canillaActual.IdCanilla, canillaActual.Legajo, canillaActual.NomApe, canillaActual.Parada, canillaActual.IdZona, + canillaActual.Accionista, canillaActual.Obs, canillaActual.Empresa, + BajaNueva = darDeBaja, FechaBajaNueva = (darDeBaja ? fechaBaja : null), // FechaBaja solo si se da de baja + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoModHist = (darDeBaja ? "Baja" : "Alta") + }, transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new { Baja = darDeBaja, FechaBaja = (darDeBaja ? fechaBaja : null), IdCanilla = id }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs new file mode 100644 index 0000000..d029907 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/DistribuidorRepository.cs @@ -0,0 +1,212 @@ +using Dapper; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class DistribuidorRepository : IDistribuidorRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public DistribuidorRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? nombreFilter, string? nroDocFilter) + { + var sqlBuilder = new StringBuilder(@" + SELECT d.*, z.Nombre AS NombreZona + FROM dbo.dist_dtDistribuidores d + LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona + WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(nombreFilter)) + { + sqlBuilder.Append(" AND d.Nombre LIKE @Nombre"); + parameters.Add("Nombre", $"%{nombreFilter}%"); + } + if (!string.IsNullOrWhiteSpace(nroDocFilter)) + { + sqlBuilder.Append(" AND d.NroDoc LIKE @NroDoc"); + parameters.Add("NroDoc", $"%{nroDocFilter}%"); + } + sqlBuilder.Append(" ORDER BY d.Nombre;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync( + sqlBuilder.ToString(), + (dist, zona) => (dist, zona), + parameters, + splitOn: "NombreZona" + ); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Distribuidores."); + return Enumerable.Empty<(Distribuidor, string?)>(); + } + } + + public async Task<(Distribuidor? Distribuidor, string? NombreZona)> GetByIdAsync(int id) + { + const string sql = @" + SELECT d.*, z.Nombre AS NombreZona + FROM dbo.dist_dtDistribuidores d + LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona + WHERE d.Id_Distribuidor = @Id"; + try + { + using var connection = _connectionFactory.CreateConnection(); + var result = await connection.QueryAsync( + sql, + (dist, zona) => (dist, zona), + new { Id = id }, + splitOn: "NombreZona" + ); + return result.SingleOrDefault(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Distribuidor por ID: {IdDistribuidor}", id); + return (null, null); + } + } + + public async Task GetByIdSimpleAsync(int id) + { + const string sql = "SELECT * FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @Id"; + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + + public async Task ExistsByNroDocAsync(string nroDoc, int? excludeIdDistribuidor = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtDistribuidores WHERE NroDoc = @NroDoc"); + var parameters = new DynamicParameters(); + parameters.Add("NroDoc", nroDoc); + if (excludeIdDistribuidor.HasValue) + { + sqlBuilder.Append(" AND Id_Distribuidor != @ExcludeId"); + parameters.Add("ExcludeId", excludeIdDistribuidor.Value); + } + using var connection = _connectionFactory.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + public async Task ExistsByNameAsync(string nombre, int? excludeIdDistribuidor = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtDistribuidores WHERE Nombre = @Nombre"); + var parameters = new DynamicParameters(); + parameters.Add("Nombre", nombre); + if (excludeIdDistribuidor.HasValue) + { + sqlBuilder.Append(" AND Id_Distribuidor != @ExcludeId"); + parameters.Add("ExcludeId", excludeIdDistribuidor.Value); + } + using var connection = _connectionFactory.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + + public async Task IsInUseAsync(int id) + { + using var connection = _connectionFactory.CreateConnection(); + string[] checkQueries = { + "SELECT TOP 1 1 FROM dbo.dist_EntradasSalidas WHERE Id_Distribuidor = @Id", + "SELECT TOP 1 1 FROM dbo.cue_PagosDistribuidor WHERE Id_Distribuidor = @Id", + "SELECT TOP 1 1 FROM dbo.dist_PorcPago WHERE Id_Distribuidor = @Id" + }; + foreach (var query in checkQueries) + { + if (await connection.ExecuteScalarAsync(query, new { Id = id }) == 1) return true; + } + return false; + } + + public async Task CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_dtDistribuidores (Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad) + OUTPUT INSERTED.* + VALUES (@Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtDistribuidores_H + (Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdDistribuidor, @Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad, @Id_Usuario, @FechaMod, @TipoMod);"; + + var connection = transaction.Connection!; + var inserted = await connection.QuerySingleAsync(sqlInsert, nuevoDistribuidor, transaction); + if (inserted == null) throw new DataException("Error al crear distribuidor."); + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + inserted.IdDistribuidor, inserted.Nombre, inserted.Contacto, inserted.NroDoc, inserted.IdZona, + inserted.Calle, inserted.Numero, inserted.Piso, inserted.Depto, inserted.Telefono, inserted.Email, inserted.Localidad, + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Creado" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(Distribuidor distribuidorAActualizar, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var actual = await connection.QuerySingleOrDefaultAsync( + "SELECT * FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdDistribuidor", + new { distribuidorAActualizar.IdDistribuidor }, transaction); + if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado."); + + const string sqlUpdate = @" + UPDATE dbo.dist_dtDistribuidores SET + Nombre = @Nombre, Contacto = @Contacto, NroDoc = @NroDoc, Id_Zona = @IdZona, Calle = @Calle, Numero = @Numero, + Piso = @Piso, Depto = @Depto, Telefono = @Telefono, Email = @Email, Localidad = @Localidad + WHERE Id_Distribuidor = @IdDistribuidor;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtDistribuidores_H + (Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdDistribuidor, @Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad, @Id_Usuario, @FechaMod, @TipoMod);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdDistribuidor = actual.IdDistribuidor, Nombre = actual.Nombre, Contacto = actual.Contacto, NroDoc = actual.NroDoc, IdZona = actual.IdZona, + Calle = actual.Calle, Numero = actual.Numero, Piso = actual.Piso, Depto = actual.Depto, Telefono = actual.Telefono, Email = actual.Email, Localidad = actual.Localidad, + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Actualizado" + }, transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, distribuidorAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var actual = await connection.QuerySingleOrDefaultAsync( + "SELECT * FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @Id", new { Id = id }, transaction); + if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado."); + + const string sqlDelete = "DELETE FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @Id"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtDistribuidores_H + (Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdDistribuidor, @Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad, @Id_Usuario, @FechaMod, @TipoMod);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdDistribuidor = actual.IdDistribuidor, actual.Nombre, actual.Contacto, actual.NroDoc, actual.IdZona, + actual.Calle, actual.Numero, actual.Piso, actual.Depto, actual.Telefono, actual.Email, actual.Localidad, + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Eliminado" + }, transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ICanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ICanillaRepository.cs new file mode 100644 index 0000000..644320a --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ICanillaRepository.cs @@ -0,0 +1,19 @@ +using GestionIntegral.Api.Models.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface ICanillaRepository + { + Task> GetAllAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos); + Task<(Canilla? Canilla, string? NombreZona, string? NombreEmpresa)> GetByIdAsync(int id); + Task GetByIdSimpleAsync(int id); // Para obtener solo la entidad Canilla + Task CreateAsync(Canilla nuevoCanilla, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(Canilla canillaAActualizar, int idUsuario, IDbTransaction transaction); + Task ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario, IDbTransaction transaction); + Task ExistsByLegajoAsync(int legajo, int? excludeIdCanilla = null); + // IsInUse no es tan directo, ya que las liquidaciones se marcan. Se podría verificar dist_EntradasSalidasCanillas no liquidadas. + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IDistribuidorRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IDistribuidorRepository.cs new file mode 100644 index 0000000..4966582 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IDistribuidorRepository.cs @@ -0,0 +1,20 @@ +using GestionIntegral.Api.Models.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IDistribuidorRepository + { + Task> GetAllAsync(string? nombreFilter, string? nroDocFilter); + Task<(Distribuidor? Distribuidor, string? NombreZona)> GetByIdAsync(int id); + Task GetByIdSimpleAsync(int id); // Para uso interno en el servicio + Task CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(Distribuidor distribuidorAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction); + Task ExistsByNroDocAsync(string nroDoc, int? excludeIdDistribuidor = null); + Task ExistsByNameAsync(string nombre, int? excludeIdDistribuidor = null); + Task IsInUseAsync(int id); // Verificar en dist_EntradasSalidas, cue_PagosDistribuidor, dist_PorcPago + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IOtroDestinoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IOtroDestinoRepository.cs new file mode 100644 index 0000000..d4cadc1 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IOtroDestinoRepository.cs @@ -0,0 +1,18 @@ +using GestionIntegral.Api.Models.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IOtroDestinoRepository + { + Task> GetAllAsync(string? nombreFilter); + Task GetByIdAsync(int id); + Task CreateAsync(OtroDestino nuevoDestino, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(OtroDestino destinoAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction); + Task ExistsByNameAsync(string nombre, int? excludeId = null); + Task IsInUseAsync(int id); // Verificar si se usa en dist_SalidasOtrosDestinos + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcMonCanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcMonCanillaRepository.cs new file mode 100644 index 0000000..b0095c8 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcMonCanillaRepository.cs @@ -0,0 +1,10 @@ +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IPorcMonCanillaRepository + { + Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcPagoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcPagoRepository.cs new file mode 100644 index 0000000..73c059d --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPorcPagoRepository.cs @@ -0,0 +1,9 @@ +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion + { + public interface IPorcPagoRepository { + Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPrecioRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPrecioRepository.cs new file mode 100644 index 0000000..fa83182 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPrecioRepository.cs @@ -0,0 +1,20 @@ +using GestionIntegral.Api.Models.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IPrecioRepository + { + Task> GetByPublicacionIdAsync(int idPublicacion); + Task GetByIdAsync(int idPrecio); + Task GetActiveByPublicacionAndDateAsync(int idPublicacion, DateTime fecha, IDbTransaction? transaction = null); + Task CreateAsync(Precio nuevoPrecio, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(Precio precioAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int idPrecio, int idUsuario, IDbTransaction transaction); + // MODIFICADO: Añadir idUsuarioAuditoria + Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); + Task GetPreviousActivePriceAsync(int idPublicacion, DateTime vigenciaDNuevo, IDbTransaction transaction); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPubliSeccionRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPubliSeccionRepository.cs new file mode 100644 index 0000000..6f3f789 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPubliSeccionRepository.cs @@ -0,0 +1,10 @@ +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IPubliSeccionRepository + { + Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPublicacionRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPublicacionRepository.cs new file mode 100644 index 0000000..504050c --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IPublicacionRepository.cs @@ -0,0 +1,19 @@ +using GestionIntegral.Api.Models.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IPublicacionRepository + { + Task> GetAllAsync(string? nombreFilter, int? idEmpresaFilter, bool? soloHabilitadas); + Task<(Publicacion? Publicacion, string? NombreEmpresa)> GetByIdAsync(int id); + Task GetByIdSimpleAsync(int id); // Para uso interno del servicio + Task CreateAsync(Publicacion nuevaPublicacion, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(Publicacion publicacionAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction); // Borrado físico con historial + Task ExistsByNameAndEmpresaAsync(string nombre, int idEmpresa, int? excludeIdPublicacion = null); + Task IsInUseAsync(int id); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IRecargoZonaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IRecargoZonaRepository.cs new file mode 100644 index 0000000..281dedf --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IRecargoZonaRepository.cs @@ -0,0 +1,10 @@ +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IRecargoZonaRepository + { + Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/OtroDestinoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/OtroDestinoRepository.cs new file mode 100644 index 0000000..cf316b0 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/OtroDestinoRepository.cs @@ -0,0 +1,189 @@ +using Dapper; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class OtroDestinoRepository : IOtroDestinoRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public OtroDestinoRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? nombreFilter) + { + var sqlBuilder = new StringBuilder("SELECT Id_Destino AS IdDestino, Nombre, Obs FROM dbo.dist_dtOtrosDestinos WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(nombreFilter)) + { + sqlBuilder.Append(" AND Nombre LIKE @NombreFilter"); + parameters.Add("NombreFilter", $"%{nombreFilter}%"); + } + sqlBuilder.Append(" ORDER BY Nombre;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Otros Destinos. Filtro: {Nombre}", nombreFilter); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int id) + { + const string sql = "SELECT Id_Destino AS IdDestino, Nombre, Obs FROM dbo.dist_dtOtrosDestinos WHERE Id_Destino = @Id"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Otro Destino por ID: {IdDestino}", id); + return null; + } + } + + public async Task ExistsByNameAsync(string nombre, int? excludeId = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtOtrosDestinos WHERE Nombre = @Nombre"); + var parameters = new DynamicParameters(); + parameters.Add("Nombre", nombre); + + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND Id_Destino != @ExcludeId"); + parameters.Add("ExcludeId", excludeId.Value); + } + + try + { + using var connection = _connectionFactory.CreateConnection(); + var count = await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + return count > 0; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByNameAsync para Otro Destino con nombre: {Nombre}", nombre); + return true; + } + } + + public async Task IsInUseAsync(int id) + { + const string sqlCheckSalidas = "SELECT TOP 1 1 FROM dbo.dist_SalidasOtrosDestinos WHERE Id_Destino = @IdDestino"; + try + { + using var connection = _connectionFactory.CreateConnection(); + var inUse = await connection.ExecuteScalarAsync(sqlCheckSalidas, new { IdDestino = id }); + return inUse.HasValue && inUse.Value == 1; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en IsInUseAsync para Otro Destino ID: {IdDestino}", id); + return true; + } + } + + public async Task CreateAsync(OtroDestino nuevoDestino, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_dtOtrosDestinos (Nombre, Obs) + OUTPUT INSERTED.Id_Destino AS IdDestino, INSERTED.Nombre, INSERTED.Obs + VALUES (@Nombre, @Obs);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtOtrosDestinos_H (Id_Destino, Nombre, Obs, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdDestino, @Nombre, @Obs, @IdUsuario, @FechaMod, @TipoMod);"; + + var connection = transaction.Connection!; + var insertedDestino = await connection.QuerySingleAsync(sqlInsert, nuevoDestino, transaction); + + if (insertedDestino == null || insertedDestino.IdDestino <= 0) + { + throw new DataException("No se pudo obtener el ID del otro destino insertado."); + } + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdDestino = insertedDestino.IdDestino, + insertedDestino.Nombre, + insertedDestino.Obs, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Insertada" + }, transaction); + return insertedDestino; + } + + public async Task UpdateAsync(OtroDestino destinoAActualizar, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var destinoActual = await connection.QuerySingleOrDefaultAsync( + "SELECT Id_Destino AS IdDestino, Nombre, Obs FROM dbo.dist_dtOtrosDestinos WHERE Id_Destino = @Id", + new { Id = destinoAActualizar.IdDestino }, transaction); + + if (destinoActual == null) throw new KeyNotFoundException($"Otro Destino con ID {destinoAActualizar.IdDestino} no encontrado."); + + const string sqlUpdate = "UPDATE dbo.dist_dtOtrosDestinos SET Nombre = @Nombre, Obs = @Obs WHERE Id_Destino = @IdDestino;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtOtrosDestinos_H (Id_Destino, Nombre, Obs, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdDestino, @NombreActual, @ObsActual, @IdUsuario, @FechaMod, @TipoMod);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdDestino = destinoActual.IdDestino, + NombreActual = destinoActual.Nombre, + ObsActual = destinoActual.Obs, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Modificada" + }, transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, destinoAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var destinoActual = await connection.QuerySingleOrDefaultAsync( + "SELECT Id_Destino AS IdDestino, Nombre, Obs FROM dbo.dist_dtOtrosDestinos WHERE Id_Destino = @Id", + new { Id = id }, transaction); + + if (destinoActual == null) throw new KeyNotFoundException($"Otro Destino con ID {id} no encontrado."); + + const string sqlDelete = "DELETE FROM dbo.dist_dtOtrosDestinos WHERE Id_Destino = @Id"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtOtrosDestinos_H (Id_Destino, Nombre, Obs, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdDestino, @Nombre, @Obs, @IdUsuario, @FechaMod, @TipoMod);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdDestino = destinoActual.IdDestino, + destinoActual.Nombre, + destinoActual.Obs, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Eliminada" + }, transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcMonCanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcMonCanillaRepository.cs new file mode 100644 index 0000000..2a0a333 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcMonCanillaRepository.cs @@ -0,0 +1,58 @@ +using Dapper; +using System.Data; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using GestionIntegral.Api.Models.Distribucion; +using System; +using System.Linq; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class PorcMonCanillaRepository : IPorcMonCanillaRepository + { + private readonly DbConnectionFactory _cf; private readonly ILogger _log; + public PorcMonCanillaRepository(DbConnectionFactory cf, ILogger log) { _cf = cf; _log = log; } + + public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) + { + const string selectSql = "SELECT * FROM dbo.dist_PorcMonPagoCanilla WHERE Id_Publicacion = @IdPublicacion"; + var itemsToDelete = await transaction.Connection!.QueryAsync(selectSql, new { IdPublicacion = idPublicacion }, transaction); + + if (itemsToDelete.Any()) + { + const string insertHistoricoSql = @" + INSERT INTO dbo.dist_PorcMonPagoCanilla_H (Id_PorcMon, Id_Publicacion, Id_Canilla, VigenciaD, VigenciaH, PorcMon, EsPorcentaje, Id_Usuario, FechaMod, TipoMod) + VALUES (@Id_PorcMon, @Id_Publicacion, @Id_Canilla, @VigenciaD, @VigenciaH, @PorcMon, @EsPorcentaje, @Id_Usuario, @FechaMod, @TipoMod);"; + + foreach (var item in itemsToDelete) + { + await transaction.Connection!.ExecuteAsync(insertHistoricoSql, new + { + Id_PorcMon = item.IdPorcMon, // Mapeo de propiedad a parámetro SQL + Id_Publicacion = item.IdPublicacion, + Id_Canilla = item.IdCanilla, + item.VigenciaD, + item.VigenciaH, + item.PorcMon, + item.EsPorcentaje, + Id_Usuario = idUsuarioAuditoria, + FechaMod = DateTime.Now, + TipoMod = "Eliminado (Cascada)" + }, transaction); + } + } + + const string deleteSql = "DELETE FROM dbo.dist_PorcMonPagoCanilla WHERE Id_Publicacion = @IdPublicacion"; + try + { + await transaction.Connection!.ExecuteAsync(deleteSql, new { IdPublicacion = idPublicacion }, transaction); + return true; + } + catch (System.Exception e) + { + _log.LogError(e, "Error al eliminar PorcMonCanilla por IdPublicacion: {idPublicacion}", idPublicacion); + throw; + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcPagoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcPagoRepository.cs new file mode 100644 index 0000000..8c2eeb8 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PorcPagoRepository.cs @@ -0,0 +1,56 @@ +using Dapper; using System.Data; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using GestionIntegral.Api.Models.Distribucion; +using System; +using System.Linq; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class PorcPagoRepository : IPorcPagoRepository + { + private readonly DbConnectionFactory _cf; private readonly ILogger _log; + public PorcPagoRepository(DbConnectionFactory cf, ILogger log) { _cf = cf; _log = log; } + + public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) + { + const string selectSql = "SELECT * FROM dbo.dist_PorcPago WHERE Id_Publicacion = @IdPublicacion"; + var itemsToDelete = await transaction.Connection!.QueryAsync(selectSql, new { IdPublicacion = idPublicacion }, transaction); + + if (itemsToDelete.Any()) + { + const string insertHistoricoSql = @" + INSERT INTO dbo.dist_PorcPago_H (Id_Porcentaje, Id_Publicacion, Id_Distribuidor, VigenciaD, VigenciaH, Porcentaje, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPorcentaje, @IdPublicacion, @IdDistribuidor, @VigenciaD, @VigenciaH, @Porcentaje, @Id_Usuario, @FechaMod, @TipoMod);"; + + foreach (var item in itemsToDelete) + { + await transaction.Connection!.ExecuteAsync(insertHistoricoSql, new + { + item.IdPorcentaje, // Mapea a @Id_Porcentaje si usas nombres con _ en SQL + item.IdPublicacion, + item.IdDistribuidor, + item.VigenciaD, + item.VigenciaH, + item.Porcentaje, + Id_Usuario = idUsuarioAuditoria, + FechaMod = DateTime.Now, + TipoMod = "Eliminado (Cascada)" + }, transaction); + } + } + + const string deleteSql = "DELETE FROM dbo.dist_PorcPago WHERE Id_Publicacion = @IdPublicacion"; + try + { + await transaction.Connection!.ExecuteAsync(deleteSql, new { IdPublicacion = idPublicacion }, transaction); + return true; + } + catch (System.Exception e) + { + _log.LogError(e, "Error al eliminar PorcPago por IdPublicacion: {idPublicacion}", idPublicacion); + throw; + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PrecioRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PrecioRepository.cs new file mode 100644 index 0000000..ee58619 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PrecioRepository.cs @@ -0,0 +1,177 @@ +using Dapper; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class PrecioRepository : IPrecioRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public PrecioRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetByPublicacionIdAsync(int idPublicacion) + { + const string sql = "SELECT * FROM dbo.dist_Precios WHERE Id_Publicacion = @IdPublicacion ORDER BY VigenciaD DESC"; + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sql, new { IdPublicacion = idPublicacion }); + } + + public async Task GetByIdAsync(int idPrecio) + { + const string sql = "SELECT * FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecio"; + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdPrecio = idPrecio }); + } + + public async Task GetActiveByPublicacionAndDateAsync(int idPublicacion, DateTime fecha, IDbTransaction? transaction = null) + { + // Obtiene el precio vigente para una publicación en una fecha específica + const string sql = @" + SELECT TOP 1 * FROM dbo.dist_Precios + WHERE Id_Publicacion = @IdPublicacion + AND VigenciaD <= @Fecha + AND (VigenciaH IS NULL OR VigenciaH >= @Fecha) + ORDER BY VigenciaD DESC;"; // Por si hay solapamientos incorrectos, tomar el más reciente + + var cn = transaction?.Connection ?? _connectionFactory.CreateConnection(); + return await cn.QuerySingleOrDefaultAsync(sql, new { IdPublicacion = idPublicacion, Fecha = fecha }, transaction); + } + + public async Task GetPreviousActivePriceAsync(int idPublicacion, DateTime vigenciaDNuevo, IDbTransaction transaction) + { + // Busca el último precio activo antes de la vigenciaD del nuevo precio, que no tenga VigenciaH o cuya VigenciaH sea mayor o igual a la nueva VigenciaD - 1 día + // y que no sea el mismo periodo que se está por crear/actualizar (si tuviera un ID). + const string sql = @" + SELECT TOP 1 * + FROM dbo.dist_Precios + WHERE Id_Publicacion = @IdPublicacion + AND VigenciaD < @VigenciaDNuevo + AND VigenciaH IS NULL + ORDER BY VigenciaD DESC;"; + return await transaction.Connection!.QuerySingleOrDefaultAsync(sql, new { IdPublicacion = idPublicacion, VigenciaDNuevo = vigenciaDNuevo }, transaction); + } + + + public async Task CreateAsync(Precio nuevoPrecio, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_Precios (Id_Publicacion, VigenciaD, VigenciaH, Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo) + OUTPUT INSERTED.* + VALUES (@IdPublicacion, @VigenciaD, @VigenciaH, @Lunes, @Martes, @Miercoles, @Jueves, @Viernes, @Sabado, @Domingo);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_Precios_H + (Id_Precio, Id_Publicacion, VigenciaD, VigenciaH, Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPrecio, @IdPublicacion, @VigenciaD, @VigenciaH, @Lunes, @Martes, @Miercoles, @Jueves, @Viernes, @Sabado, @Domingo, @Id_Usuario, @FechaMod, @TipoMod);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevoPrecio, transaction); + if (inserted == null) throw new DataException("Error al crear precio."); + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + inserted.IdPrecio, inserted.IdPublicacion, inserted.VigenciaD, inserted.VigenciaH, + inserted.Lunes, inserted.Martes, inserted.Miercoles, inserted.Jueves, inserted.Viernes, inserted.Sabado, inserted.Domingo, + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Creado" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(Precio precioAActualizar, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + "SELECT * FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecio", + new { precioAActualizar.IdPrecio }, transaction); + if (actual == null) throw new KeyNotFoundException("Precio no encontrado."); + + const string sqlUpdate = @" + UPDATE dbo.dist_Precios SET + VigenciaH = @VigenciaH, Lunes = @Lunes, Martes = @Martes, Miercoles = @Miercoles, + Jueves = @Jueves, Viernes = @Viernes, Sabado = @Sabado, Domingo = @Domingo + -- No se permite cambiar IdPublicacion ni VigenciaD de un registro de precio existente + WHERE Id_Precio = @IdPrecio;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_Precios_H + (Id_Precio, Id_Publicacion, VigenciaD, VigenciaH, Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPrecio, @IdPublicacion, @VigenciaD, @VigenciaH, @Lunes, @Martes, @Miercoles, @Jueves, @Viernes, @Sabado, @Domingo, @Id_Usuario, @FechaMod, @TipoMod);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + actual.IdPrecio, actual.IdPublicacion, actual.VigenciaD, // VigenciaD actual para el historial + VigenciaH = actual.VigenciaH, // VigenciaH actual para el historial + Lunes = actual.Lunes, Martes = actual.Martes, Miercoles = actual.Miercoles, Jueves = actual.Jueves, + Viernes = actual.Viernes, Sabado = actual.Sabado, Domingo = actual.Domingo, // Precios actuales para el historial + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Actualizado" // O "Cerrado" si solo se actualiza VigenciaH + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, precioAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int idPrecio, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + "SELECT * FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecio", new { IdPrecio = idPrecio }, transaction); + if (actual == null) throw new KeyNotFoundException("Precio no encontrado para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.dist_Precios WHERE Id_Precio = @IdPrecio"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_Precios_H + (Id_Precio, Id_Publicacion, VigenciaD, VigenciaH, Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPrecio, @IdPublicacion, @VigenciaD, @VigenciaH, @Lunes, @Martes, @Miercoles, @Jueves, @Viernes, @Sabado, @Domingo, @Id_Usuario, @FechaMod, @TipoMod);"; + + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + actual.IdPrecio, actual.IdPublicacion, actual.VigenciaD, actual.VigenciaH, + actual.Lunes, actual.Martes, actual.Miercoles, actual.Jueves, actual.Viernes, actual.Sabado, actual.Domingo, + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Eliminado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdPrecio = idPrecio }, transaction); + return rowsAffected == 1; + } + + public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) // MODIFICADO: Recibe idUsuarioAuditoria + { + const string selectPrecios = "SELECT * FROM dbo.dist_Precios WHERE Id_Publicacion = @IdPublicacion"; + var preciosAEliminar = await transaction.Connection!.QueryAsync(selectPrecios, new { IdPublicacion = idPublicacion }, transaction); + + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_Precios_H + (Id_Precio, Id_Publicacion, VigenciaD, VigenciaH, Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPrecio, @IdPublicacion, @VigenciaD, @VigenciaH, @Lunes, @Martes, @Miercoles, @Jueves, @Viernes, @Sabado, @Domingo, @Id_Usuario, @FechaMod, @TipoMod);"; + + foreach (var precio in preciosAEliminar) + { + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + precio.IdPrecio, precio.IdPublicacion, precio.VigenciaD, precio.VigenciaH, + precio.Lunes, precio.Martes, precio.Miercoles, precio.Jueves, precio.Viernes, precio.Sabado, precio.Domingo, + Id_Usuario = idUsuarioAuditoria, // MODIFICADO: Usar el idUsuarioAuditoria pasado + FechaMod = DateTime.Now, TipoMod = "Eliminado (Cascada)" + }, transaction); + } + + const string sql = "DELETE FROM dbo.dist_Precios WHERE Id_Publicacion = @IdPublicacion"; + try + { + var rowsAffected = await transaction.Connection!.ExecuteAsync(sql, new { IdPublicacion = idPublicacion }, transaction: transaction); + // No necesitamos devolver rowsAffected >= 0 si la lógica del servicio ya valida si debe haber registros + return true; // Indica que la operación de borrado (incluyendo 0 filas) se intentó + } + catch (System.Exception ex) + { + _logger.LogError(ex, "Error al eliminar precios por IdPublicacion: {IdPublicacion}", idPublicacion); + throw; + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PubliSeccionRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PubliSeccionRepository.cs new file mode 100644 index 0000000..2bc5b46 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PubliSeccionRepository.cs @@ -0,0 +1,55 @@ +using Dapper; +using System.Data; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using GestionIntegral.Api.Models.Distribucion; +using System; +using System.Linq; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class PubliSeccionRepository : IPubliSeccionRepository + { + private readonly DbConnectionFactory _cf; private readonly ILogger _log; + public PubliSeccionRepository(DbConnectionFactory cf, ILogger log) { _cf = cf; _log = log; } + + public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) + { + const string selectSql = "SELECT * FROM dbo.dist_dtPubliSecciones WHERE Id_Publicacion = @IdPublicacion"; + var itemsToDelete = await transaction.Connection!.QueryAsync(selectSql, new { IdPublicacion = idPublicacion }, transaction); + + if (itemsToDelete.Any()) + { + const string insertHistoricoSql = @" + INSERT INTO dbo.dist_dtPubliSecciones_H (Id_Seccion, Id_Publicacion, Nombre, Estado, Id_Usuario, FechaMod, TipoMod) + VALUES (@Id_Seccion, @Id_Publicacion, @Nombre, @Estado, @Id_Usuario, @FechaMod, @TipoMod);"; + + foreach (var item in itemsToDelete) + { + await transaction.Connection!.ExecuteAsync(insertHistoricoSql, new + { + Id_Seccion = item.IdSeccion, // Mapeo de propiedad a parámetro SQL + Id_Publicacion = item.IdPublicacion, + item.Nombre, + item.Estado, + Id_Usuario = idUsuarioAuditoria, + FechaMod = DateTime.Now, + TipoMod = "Eliminado (Cascada)" + }, transaction); + } + } + + const string deleteSql = "DELETE FROM dbo.dist_dtPubliSecciones WHERE Id_Publicacion = @IdPublicacion"; + try + { + await transaction.Connection!.ExecuteAsync(deleteSql, new { IdPublicacion = idPublicacion }, transaction); + return true; + } + catch (System.Exception e) + { + _log.LogError(e, "Error al eliminar PubliSecciones por IdPublicacion: {idPublicacion}", idPublicacion); + throw; + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PublicacionRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PublicacionRepository.cs new file mode 100644 index 0000000..ec102ef --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/PublicacionRepository.cs @@ -0,0 +1,219 @@ +using Dapper; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class PublicacionRepository : IPublicacionRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public PublicacionRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? nombreFilter, int? idEmpresaFilter, bool? soloHabilitadas) + { + var sqlBuilder = new StringBuilder(@" + SELECT p.*, e.Nombre AS NombreEmpresa + FROM dbo.dist_dtPublicaciones p + INNER JOIN dbo.dist_dtEmpresas e ON p.Id_Empresa = e.Id_Empresa + WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (soloHabilitadas.HasValue) + { + sqlBuilder.Append(soloHabilitadas.Value ? " AND p.Habilitada = 1" : " AND (p.Habilitada = 0 OR p.Habilitada IS NULL)"); + } + if (!string.IsNullOrWhiteSpace(nombreFilter)) + { + sqlBuilder.Append(" AND p.Nombre LIKE @Nombre"); + parameters.Add("Nombre", $"%{nombreFilter}%"); + } + if (idEmpresaFilter.HasValue && idEmpresaFilter > 0) + { + sqlBuilder.Append(" AND p.Id_Empresa = @IdEmpresa"); + parameters.Add("IdEmpresa", idEmpresaFilter.Value); + } + sqlBuilder.Append(" ORDER BY p.Nombre;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync( + sqlBuilder.ToString(), + (pub, empNombre) => (pub, empNombre), + parameters, + splitOn: "NombreEmpresa" + ); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todas las Publicaciones."); + return Enumerable.Empty<(Publicacion, string)>(); + } + } + + public async Task<(Publicacion? Publicacion, string? NombreEmpresa)> GetByIdAsync(int id) + { + const string sql = @" + SELECT p.*, e.Nombre AS NombreEmpresa + FROM dbo.dist_dtPublicaciones p + INNER JOIN dbo.dist_dtEmpresas e ON p.Id_Empresa = e.Id_Empresa + WHERE p.Id_Publicacion = @Id"; + try + { + using var connection = _connectionFactory.CreateConnection(); + var result = await connection.QueryAsync( + sql, + (pub, empNombre) => (pub, empNombre), + new { Id = id }, + splitOn: "NombreEmpresa" + ); + return result.SingleOrDefault(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Publicación por ID: {IdPublicacion}", id); + return (null, null); + } + } + public async Task GetByIdSimpleAsync(int id) + { + const string sql = "SELECT * FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @Id"; + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + + + public async Task ExistsByNameAndEmpresaAsync(string nombre, int idEmpresa, int? excludeIdPublicacion = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtPublicaciones WHERE Nombre = @Nombre AND Id_Empresa = @IdEmpresa"); + var parameters = new DynamicParameters(); + parameters.Add("Nombre", nombre); + parameters.Add("IdEmpresa", idEmpresa); + if (excludeIdPublicacion.HasValue) + { + sqlBuilder.Append(" AND Id_Publicacion != @ExcludeId"); + parameters.Add("ExcludeId", excludeIdPublicacion.Value); + } + using var connection = _connectionFactory.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + + public async Task IsInUseAsync(int id) + { + // Verificar en tablas relacionadas: dist_EntradasSalidas, dist_EntradasSalidasCanillas, + // dist_Precios, dist_RecargoZona, dist_PorcPago, dist_PorcMonPagoCanilla, dist_dtPubliSecciones, + // bob_RegPublicaciones, bob_StockBobinas (donde Id_Publicacion se usa) + using var connection = _connectionFactory.CreateConnection(); + string[] checkQueries = { + "SELECT TOP 1 1 FROM dbo.dist_EntradasSalidas WHERE Id_Publicacion = @Id", + "SELECT TOP 1 1 FROM dbo.dist_EntradasSalidasCanillas WHERE Id_Publicacion = @Id", + "SELECT TOP 1 1 FROM dbo.dist_Precios WHERE Id_Publicacion = @Id", + "SELECT TOP 1 1 FROM dbo.dist_RecargoZona WHERE Id_Publicacion = @Id", + "SELECT TOP 1 1 FROM dbo.dist_PorcPago WHERE Id_Publicacion = @Id", + "SELECT TOP 1 1 FROM dbo.dist_PorcMonPagoCanilla WHERE Id_Publicacion = @Id", + "SELECT TOP 1 1 FROM dbo.dist_dtPubliSecciones WHERE Id_Publicacion = @Id", + "SELECT TOP 1 1 FROM dbo.bob_RegPublicaciones WHERE Id_Publicacion = @Id", + "SELECT TOP 1 1 FROM dbo.bob_StockBobinas WHERE Id_Publicacion = @Id" + }; + foreach (var query in checkQueries) + { + if (await connection.ExecuteScalarAsync(query, new { Id = id }) == 1) return true; + } + return false; + } + + + public async Task CreateAsync(Publicacion nuevaPublicacion, int idUsuario, IDbTransaction transaction) + { + // Habilitada por defecto es true si es null en el modelo + nuevaPublicacion.Habilitada ??= true; + + const string sqlInsert = @" + INSERT INTO dbo.dist_dtPublicaciones (Nombre, Observacion, Id_Empresa, CtrlDevoluciones, Habilitada) + OUTPUT INSERTED.* + VALUES (@Nombre, @Observacion, @IdEmpresa, @CtrlDevoluciones, @Habilitada);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtPublicaciones_H + (Id_Publicacion, Nombre, Observacion, Id_Empresa, Habilitada, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPublicacion, @Nombre, @Observacion, @IdEmpresa, @Habilitada, @Id_Usuario, @FechaMod, @TipoMod);"; + + var connection = transaction.Connection!; + var inserted = await connection.QuerySingleAsync(sqlInsert, nuevaPublicacion, transaction); + if (inserted == null) throw new DataException("Error al crear la publicación."); + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + inserted.IdPublicacion, inserted.Nombre, inserted.Observacion, inserted.IdEmpresa, + Habilitada = inserted.Habilitada ?? true, // Asegurar que no sea null para el historial + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Creada" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(Publicacion publicacionAActualizar, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var actual = await connection.QuerySingleOrDefaultAsync( + "SELECT * FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @IdPublicacion", + new { publicacionAActualizar.IdPublicacion }, transaction); + if (actual == null) throw new KeyNotFoundException("Publicación no encontrada."); + + publicacionAActualizar.Habilitada ??= true; // Asegurar que no sea null + + const string sqlUpdate = @" + UPDATE dbo.dist_dtPublicaciones SET + Nombre = @Nombre, Observacion = @Observacion, Id_Empresa = @IdEmpresa, + CtrlDevoluciones = @CtrlDevoluciones, Habilitada = @Habilitada + WHERE Id_Publicacion = @IdPublicacion;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtPublicaciones_H + (Id_Publicacion, Nombre, Observacion, Id_Empresa, Habilitada, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPublicacion, @Nombre, @Observacion, @IdEmpresa, @Habilitada, @Id_Usuario, @FechaMod, @TipoMod);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdPublicacion = actual.IdPublicacion, Nombre = actual.Nombre, Observacion = actual.Observacion, + IdEmpresa = actual.IdEmpresa, Habilitada = actual.Habilitada ?? true, + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Actualizada" + }, transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, publicacionAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var actual = await connection.QuerySingleOrDefaultAsync( + "SELECT * FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @Id", new { Id = id }, transaction); + if (actual == null) throw new KeyNotFoundException("Publicación no encontrada."); + + const string sqlDelete = "DELETE FROM dbo.dist_dtPublicaciones WHERE Id_Publicacion = @Id"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtPublicaciones_H + (Id_Publicacion, Nombre, Observacion, Id_Empresa, Habilitada, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPublicacion, @Nombre, @Observacion, @IdEmpresa, @Habilitada, @Id_Usuario, @FechaMod, @TipoMod);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdPublicacion = actual.IdPublicacion, actual.Nombre, actual.Observacion, actual.IdEmpresa, + Habilitada = actual.Habilitada ?? true, + Id_Usuario = idUsuario, FechaMod = DateTime.Now, TipoMod = "Eliminada" + }, transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/RecargoZonaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/RecargoZonaRepository.cs new file mode 100644 index 0000000..1f260f4 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/RecargoZonaRepository.cs @@ -0,0 +1,64 @@ +// src/Data/Repositories/RecargoZonaRepository.cs +using Dapper; +using System.Data; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using GestionIntegral.Api.Models.Distribucion; // Asegúrate que esta directiva using esté presente + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class RecargoZonaRepository : IRecargoZonaRepository + { + private readonly DbConnectionFactory _connectionFactory; // _cf + private readonly ILogger _logger; // _log + + public RecargoZonaRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task DeleteByPublicacionIdAsync(int idPublicacion, int idUsuarioAuditoria, IDbTransaction transaction) + { + // Obtener recargos para el historial + const string selectRecargos = "SELECT * FROM dbo.dist_RecargoZona WHERE Id_Publicacion = @IdPublicacion"; + var recargosAEliminar = await transaction.Connection!.QueryAsync(selectRecargos, new { IdPublicacion = idPublicacion }, transaction); + + // Asume que tienes una tabla dist_RecargoZona_H y un modelo RecargoZonaHistorico + const string sqlInsertHistorico = @" + INSERT INTO dbo.dist_RecargoZona_H (Id_Recargo, Id_Publicacion, Id_Zona, VigenciaD, VigenciaH, Valor, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdRecargo, @IdPublicacion, @IdZona, @VigenciaD, @VigenciaH, @Valor, @Id_Usuario, @FechaMod, @TipoMod);"; // Nombres de parámetros corregidos + + foreach (var recargo in recargosAEliminar) + { + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new + { + // Mapear los campos de 'recargo' a los parámetros de sqlInsertHistorico + recargo.IdRecargo, // Usar las propiedades del modelo RecargoZona + recargo.IdPublicacion, + recargo.IdZona, + recargo.VigenciaD, + recargo.VigenciaH, + recargo.Valor, + Id_Usuario = idUsuarioAuditoria, // Este es el parámetro esperado por la query + FechaMod = DateTime.Now, + TipoMod = "Eliminado (Cascada)" + }, transaction); + } + + const string sql = "DELETE FROM dbo.dist_RecargoZona WHERE Id_Publicacion = @IdPublicacion"; + try + { + await transaction.Connection!.ExecuteAsync(sql, new { IdPublicacion = idPublicacion }, transaction: transaction); + return true; // Se intentó la operación + } + catch (System.Exception ex) + { + _logger.LogError(ex, "Error al eliminar RecargosZona por IdPublicacion: {IdPublicacion}", idPublicacion); + throw; // Re-lanzar para que la transacción padre haga rollback + } + } + // Aquí irían otros métodos CRUD para RecargoZona si se gestionan individualmente. + // Por ejemplo, GetByPublicacionId, Create, Update, Delete (para un recargo específico). + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/AuthRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/AuthRepository.cs similarity index 97% rename from Backend/GestionIntegral.Api/Data/AuthRepository.cs rename to Backend/GestionIntegral.Api/Data/Repositories/Usuarios/AuthRepository.cs index 326fd84..0cc78af 100644 --- a/Backend/GestionIntegral.Api/Data/AuthRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/AuthRepository.cs @@ -1,8 +1,8 @@ using Dapper; -using GestionIntegral.Api.Models; +using GestionIntegral.Api.Models.Usuarios; using System.Data; -namespace GestionIntegral.Api.Data +namespace GestionIntegral.Api.Data.Repositories.Usuarios { public class AuthRepository : IAuthRepository { diff --git a/Backend/GestionIntegral.Api/Data/IAuthRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IAuthRepository.cs similarity index 78% rename from Backend/GestionIntegral.Api/Data/IAuthRepository.cs rename to Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IAuthRepository.cs index 9ee32c0..fa2d519 100644 --- a/Backend/GestionIntegral.Api/Data/IAuthRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IAuthRepository.cs @@ -1,6 +1,6 @@ -using GestionIntegral.Api.Models; +using GestionIntegral.Api.Models.Usuarios; -namespace GestionIntegral.Api.Data +namespace GestionIntegral.Api.Data.Repositories.Usuarios { public interface IAuthRepository { diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IPerfilRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IPerfilRepository.cs new file mode 100644 index 0000000..af15327 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IPerfilRepository.cs @@ -0,0 +1,20 @@ +using GestionIntegral.Api.Models.Usuarios; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; // Para IDbTransaction + +namespace GestionIntegral.Api.Data.Repositories.Usuarios +{ + public interface IPerfilRepository + { + Task> GetAllAsync(string? nombreFilter); + Task GetByIdAsync(int id); + Task CreateAsync(Perfil nuevoPerfil, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(Perfil perfilAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction); + Task ExistsByNameAsync(string nombrePerfil, int? excludeId = null); + Task IsInUseAsync(int id); + Task> GetPermisoIdsByPerfilIdAsync(int idPerfil); + Task UpdatePermisosByPerfilIdAsync(int idPerfil, IEnumerable nuevosPermisosIds, IDbTransaction transaction); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IPermisoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IPermisoRepository.cs new file mode 100644 index 0000000..1a1c93a --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IPermisoRepository.cs @@ -0,0 +1,19 @@ +using GestionIntegral.Api.Models.Usuarios; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; + +namespace GestionIntegral.Api.Data.Repositories.Usuarios +{ + public interface IPermisoRepository + { + Task> GetAllAsync(string? moduloFilter, string? codAccFilter); + Task GetByIdAsync(int id); + Task CreateAsync(Permiso nuevoPermiso, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(Permiso permisoAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction); + Task ExistsByCodAccAsync(string codAcc, int? excludeId = null); + Task IsInUseAsync(int id); + Task> GetPermisosByIdsAsync(IEnumerable ids); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IUsuarioRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IUsuarioRepository.cs new file mode 100644 index 0000000..42b217a --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IUsuarioRepository.cs @@ -0,0 +1,25 @@ +using GestionIntegral.Api.Models.Usuarios; // Para Usuario +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; + +namespace GestionIntegral.Api.Data.Repositories.Usuarios +{ + public interface IUsuarioRepository + { + Task> GetAllAsync(string? userFilter, string? nombreFilter); + Task GetByIdAsync(int id); + Task GetByUsernameAsync(string username); // Ya existe en IAuthRepository, pero lo duplicamos para cohesión del CRUD + Task CreateAsync(Usuario nuevoUsuario, int idUsuarioCreador, IDbTransaction transaction); + Task UpdateAsync(Usuario usuarioAActualizar, int idUsuarioModificador, IDbTransaction transaction); + // DeleteAsync no es común para usuarios, se suelen deshabilitar. + // Task DeleteAsync(int id, int idUsuarioModificador, IDbTransaction transaction); + Task SetPasswordAsync(int userId, string newHash, string newSalt, bool debeCambiarClave, int idUsuarioModificador, IDbTransaction transaction); + Task UserExistsAsync(string username, int? excludeId = null); + + // Para el DTO de listado + Task> GetAllWithProfileNameAsync(string? userFilter, string? nombreFilter); + Task<(Usuario? Usuario, string? NombrePerfil)> GetByIdWithProfileNameAsync(int id); + + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/PerfilRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/PerfilRepository.cs new file mode 100644 index 0000000..b0109d8 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/PerfilRepository.cs @@ -0,0 +1,250 @@ +using Dapper; +using GestionIntegral.Api.Models.Usuarios; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Usuarios +{ + public class PerfilRepository : IPerfilRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public PerfilRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? nombreFilter) + { + var sqlBuilder = new StringBuilder("SELECT id AS Id, perfil AS NombrePerfil, descPerfil AS Descripcion FROM dbo.gral_Perfiles WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(nombreFilter)) + { + sqlBuilder.Append(" AND perfil LIKE @NombreFilter"); + parameters.Add("NombreFilter", $"%{nombreFilter}%"); + } + sqlBuilder.Append(" ORDER BY perfil;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Perfiles. Filtro: {NombreFilter}", nombreFilter); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int id) + { + const string sql = "SELECT id AS Id, perfil AS NombrePerfil, descPerfil AS Descripcion FROM dbo.gral_Perfiles WHERE id = @Id"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Perfil por ID: {IdPerfil}", id); + return null; + } + } + + public async Task ExistsByNameAsync(string nombrePerfil, int? excludeId = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.gral_Perfiles WHERE perfil = @NombrePerfil"); + var parameters = new DynamicParameters(); + parameters.Add("NombrePerfil", nombrePerfil); + + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND id != @ExcludeId"); + parameters.Add("ExcludeId", excludeId.Value); + } + + try + { + using var connection = _connectionFactory.CreateConnection(); + var count = await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + return count > 0; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByNameAsync para Perfil con nombre: {NombrePerfil}", nombrePerfil); + return true; + } + } + + public async Task IsInUseAsync(int id) + { + const string sqlCheckUsuarios = "SELECT TOP 1 1 FROM dbo.gral_Usuarios WHERE IdPerfil = @IdPerfil"; + const string sqlCheckPermisos = "SELECT TOP 1 1 FROM dbo.gral_PermisosPerfiles WHERE idPerfil = @IdPerfil"; + try + { + using var connection = _connectionFactory.CreateConnection(); + var inUsuarios = await connection.ExecuteScalarAsync(sqlCheckUsuarios, new { IdPerfil = id }); + if (inUsuarios.HasValue && inUsuarios.Value == 1) return true; + + var inPermisos = await connection.ExecuteScalarAsync(sqlCheckPermisos, new { IdPerfil = id }); + return inPermisos.HasValue && inPermisos.Value == 1; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en IsInUseAsync para Perfil ID: {IdPerfil}", id); + return true; + } + } + + public async Task CreateAsync(Perfil nuevoPerfil, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.gral_Perfiles (perfil, descPerfil) + OUTPUT INSERTED.id AS Id, INSERTED.perfil AS NombrePerfil, INSERTED.descPerfil AS Descripcion + VALUES (@NombrePerfil, @Descripcion);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.gral_Perfiles_H (idPerfil, perfil, descPerfil, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPerfilHist, @NombrePerfilHist, @DescripcionHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + var connection = transaction.Connection!; // La conexión debe venir de la transacción + + var insertedPerfil = await connection.QuerySingleAsync( + sqlInsert, + new { nuevoPerfil.NombrePerfil, nuevoPerfil.Descripcion }, + transaction: transaction); + + if (insertedPerfil == null || insertedPerfil.Id <= 0) + { + throw new DataException("No se pudo obtener el ID del perfil insertado."); + } + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdPerfilHist = insertedPerfil.Id, // Correcto + NombrePerfilHist = insertedPerfil.NombrePerfil, + DescripcionHist = insertedPerfil.Descripcion, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Insertada" + }, transaction: transaction); + + return insertedPerfil; + } + + public async Task UpdateAsync(Perfil perfilAActualizar, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + // Obtener el estado actual PARA EL HISTORIAL DENTRO DE LA MISMA TRANSACCIÓN + var perfilActual = await connection.QuerySingleOrDefaultAsync( + "SELECT id AS Id, perfil AS NombrePerfil, descPerfil AS Descripcion FROM dbo.gral_Perfiles WHERE id = @Id", + new { Id = perfilAActualizar.Id }, + transaction); + + if (perfilActual == null) + { + // Esto no debería pasar si el servicio verifica la existencia antes, pero es una salvaguarda. + throw new KeyNotFoundException($"Perfil con ID {perfilAActualizar.Id} no encontrado para actualizar."); + } + + const string sqlUpdate = "UPDATE dbo.gral_Perfiles SET perfil = @NombrePerfil, descPerfil = @Descripcion WHERE id = @Id;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.gral_Perfiles_H (idPerfil, perfil, descPerfil, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPerfilHist, @NombrePerfilHist, @DescripcionHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + // Insertar en historial con los valores ANTES de la modificación + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdPerfilHist = perfilActual.Id, + NombrePerfilHist = perfilActual.NombrePerfil, + DescripcionHist = perfilActual.Descripcion, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Modificada" + }, transaction: transaction); + + // Actualizar la tabla principal + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, perfilAActualizar, transaction: transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + // Obtener el estado actual PARA EL HISTORIAL DENTRO DE LA MISMA TRANSACCIÓN + var perfilActual = await connection.QuerySingleOrDefaultAsync( + "SELECT id AS Id, perfil AS NombrePerfil, descPerfil AS Descripcion FROM dbo.gral_Perfiles WHERE id = @Id", + new { Id = id }, + transaction); + + if (perfilActual == null) + { + throw new KeyNotFoundException($"Perfil con ID {id} no encontrado para eliminar."); + } + + const string sqlDelete = "DELETE FROM dbo.gral_Perfiles WHERE id = @Id"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.gral_Perfiles_H (idPerfil, perfil, descPerfil, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPerfilHist, @NombrePerfilHist, @DescripcionHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + // Insertar en historial con los valores ANTES de la eliminación + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdPerfilHist = perfilActual.Id, + NombrePerfilHist = perfilActual.NombrePerfil, + DescripcionHist = perfilActual.Descripcion, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Eliminada" + }, transaction: transaction); + + // Eliminar de la tabla principal + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction: transaction); + return rowsAffected == 1; + } + public async Task> GetPermisoIdsByPerfilIdAsync(int idPerfil) + { + const string sql = "SELECT idPermiso FROM dbo.gral_PermisosPerfiles WHERE idPerfil = @IdPerfil"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sql, new { IdPerfil = idPerfil }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener IDs de permisos para el Perfil ID: {IdPerfil}", idPerfil); + return Enumerable.Empty(); + } + } + + public async Task UpdatePermisosByPerfilIdAsync(int idPerfil, IEnumerable nuevosPermisosIds, IDbTransaction transaction) + { + var connection = transaction.Connection!; + + // 1. Eliminar todos los permisos existentes para este perfil (dentro de la transacción) + const string sqlDelete = "DELETE FROM dbo.gral_PermisosPerfiles WHERE idPerfil = @IdPerfil"; + await connection.ExecuteAsync(sqlDelete, new { IdPerfil = idPerfil }, transaction: transaction); + + // 2. Insertar los nuevos permisos (si hay alguno) + if (nuevosPermisosIds != null && nuevosPermisosIds.Any()) + { + const string sqlInsert = "INSERT INTO dbo.gral_PermisosPerfiles (idPerfil, idPermiso) VALUES (@IdPerfil, @IdPermiso)"; + // Dapper puede manejar una lista de objetos para inserciones múltiples + var permisosParaInsertar = nuevosPermisosIds.Select(idPermiso => new { IdPerfil = idPerfil, IdPermiso = idPermiso }); + await connection.ExecuteAsync(sqlInsert, permisosParaInsertar, transaction: transaction); + } + // No hay tabla _H para gral_PermisosPerfiles directamente en este diseño. + // La auditoría de qué usuario cambió los permisos de un perfil se podría registrar + // en una tabla de log de acciones más general si fuera necesario, o deducir + // indirectamente si la interfaz de usuario solo permite a SuperAdmin hacer esto. + } + } + +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/PermisoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/PermisoRepository.cs new file mode 100644 index 0000000..6f47ecc --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/PermisoRepository.cs @@ -0,0 +1,231 @@ +using Dapper; +using GestionIntegral.Api.Models.Usuarios; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Usuarios +{ + public class PermisoRepository : IPermisoRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public PermisoRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? moduloFilter, string? codAccFilter) + { + var sqlBuilder = new StringBuilder("SELECT id AS Id, modulo, descPermiso, codAcc FROM dbo.gral_Permisos WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(moduloFilter)) + { + sqlBuilder.Append(" AND modulo LIKE @ModuloFilter"); + parameters.Add("ModuloFilter", $"%{moduloFilter}%"); + } + if (!string.IsNullOrWhiteSpace(codAccFilter)) + { + sqlBuilder.Append(" AND codAcc LIKE @CodAccFilter"); + parameters.Add("CodAccFilter", $"%{codAccFilter}%"); + } + sqlBuilder.Append(" ORDER BY modulo, codAcc;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Permisos. Filtros: Modulo={Modulo}, CodAcc={CodAcc}", moduloFilter, codAccFilter); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int id) + { + const string sql = "SELECT id AS Id, modulo, descPermiso, codAcc FROM dbo.gral_Permisos WHERE id = @Id"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Permiso por ID: {IdPermiso}", id); + return null; + } + } + + public async Task ExistsByCodAccAsync(string codAcc, int? excludeId = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.gral_Permisos WHERE codAcc = @CodAcc"); + var parameters = new DynamicParameters(); + parameters.Add("CodAcc", codAcc); + + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND id != @ExcludeId"); + parameters.Add("ExcludeId", excludeId.Value); + } + + try + { + using var connection = _connectionFactory.CreateConnection(); + var count = await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + return count > 0; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByCodAccAsync para Permiso con codAcc: {CodAcc}", codAcc); + return true; + } + } + + public async Task IsInUseAsync(int id) + { + const string sqlCheckPermisosPerfiles = "SELECT TOP 1 1 FROM dbo.gral_PermisosPerfiles WHERE idPermiso = @IdPermiso"; + try + { + using var connection = _connectionFactory.CreateConnection(); + var inUse = await connection.ExecuteScalarAsync(sqlCheckPermisosPerfiles, new { IdPermiso = id }); + return inUse.HasValue && inUse.Value == 1; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en IsInUseAsync para Permiso ID: {IdPermiso}", id); + return true; + } + } + + public async Task> GetPermisosByIdsAsync(IEnumerable ids) + { + if (ids == null || !ids.Any()) + { + return Enumerable.Empty(); + } + const string sql = "SELECT id AS Id, modulo, descPermiso, codAcc FROM dbo.gral_Permisos WHERE id IN @Ids;"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sql, new { Ids = ids }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener permisos por IDs."); + return Enumerable.Empty(); + } + } + + + public async Task CreateAsync(Permiso nuevoPermiso, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.gral_Permisos (modulo, descPermiso, codAcc) + OUTPUT INSERTED.id AS Id, INSERTED.modulo, INSERTED.descPermiso, INSERTED.codAcc + VALUES (@Modulo, @DescPermiso, @CodAcc);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.gral_Permisos_H (idPermiso, modulo, descPermiso, codAcc, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPermisoHist, @ModuloHist, @DescPermisoHist, @CodAccHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + var connection = transaction.Connection!; + var insertedPermiso = await connection.QuerySingleAsync( + sqlInsert, + nuevoPermiso, + transaction: transaction); + + if (insertedPermiso == null || insertedPermiso.Id <= 0) + { + throw new DataException("No se pudo obtener el ID del permiso insertado."); + } + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdPermisoHist = insertedPermiso.Id, + ModuloHist = insertedPermiso.Modulo, + DescPermisoHist = insertedPermiso.DescPermiso, + CodAccHist = insertedPermiso.CodAcc, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Insertada" + }, transaction: transaction); + + return insertedPermiso; + } + + public async Task UpdateAsync(Permiso permisoAActualizar, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var permisoActual = await connection.QuerySingleOrDefaultAsync( + "SELECT id AS Id, modulo, descPermiso, codAcc FROM dbo.gral_Permisos WHERE id = @Id", + new { Id = permisoAActualizar.Id }, + transaction); + + if (permisoActual == null) + { + throw new KeyNotFoundException($"Permiso con ID {permisoAActualizar.Id} no encontrado para actualizar."); + } + + const string sqlUpdate = @"UPDATE dbo.gral_Permisos + SET modulo = @Modulo, descPermiso = @DescPermiso, codAcc = @CodAcc + WHERE id = @Id;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.gral_Permisos_H (idPermiso, modulo, descPermiso, codAcc, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPermisoHist, @ModuloHist, @DescPermisoHist, @CodAccHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdPermisoHist = permisoActual.Id, + ModuloHist = permisoActual.Modulo, + DescPermisoHist = permisoActual.DescPermiso, + CodAccHist = permisoActual.CodAcc, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Modificada" + }, transaction: transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, permisoAActualizar, transaction: transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var permisoActual = await connection.QuerySingleOrDefaultAsync( + "SELECT id AS Id, modulo, descPermiso, codAcc FROM dbo.gral_Permisos WHERE id = @Id", + new { Id = id }, + transaction); + + if (permisoActual == null) + { + throw new KeyNotFoundException($"Permiso con ID {id} no encontrado para eliminar."); + } + + const string sqlDelete = "DELETE FROM dbo.gral_Permisos WHERE id = @Id"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.gral_Permisos_H (idPermiso, modulo, descPermiso, codAcc, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPermisoHist, @ModuloHist, @DescPermisoHist, @CodAccHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdPermisoHist = permisoActual.Id, + ModuloHist = permisoActual.Modulo, + DescPermisoHist = permisoActual.DescPermiso, + CodAccHist = permisoActual.CodAcc, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Eliminada" + }, transaction: transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction: transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/UsuarioRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/UsuarioRepository.cs new file mode 100644 index 0000000..ef1c3d8 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/UsuarioRepository.cs @@ -0,0 +1,293 @@ +using Dapper; +using GestionIntegral.Api.Models.Usuarios; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Usuarios +{ + public class UsuarioRepository : IUsuarioRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public UsuarioRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? userFilter, string? nombreFilter) + { + var sqlBuilder = new StringBuilder("SELECT * FROM dbo.gral_Usuarios WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(userFilter)) + { + sqlBuilder.Append(" AND [User] LIKE @UserParam"); + parameters.Add("UserParam", $"%{userFilter}%"); + } + if (!string.IsNullOrWhiteSpace(nombreFilter)) + { + sqlBuilder.Append(" AND (Nombre LIKE @NombreParam OR Apellido LIKE @NombreParam)"); + parameters.Add("NombreParam", $"%{nombreFilter}%"); + } + sqlBuilder.Append(" ORDER BY [User];"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Usuarios."); + return Enumerable.Empty(); + } + } + + public async Task> GetAllWithProfileNameAsync(string? userFilter, string? nombreFilter) + { + var sqlBuilder = new StringBuilder(@" + SELECT u.*, p.perfil AS NombrePerfil + FROM dbo.gral_Usuarios u + INNER JOIN dbo.gral_Perfiles p ON u.IdPerfil = p.id + WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(userFilter)) + { + sqlBuilder.Append(" AND u.[User] LIKE @UserParam"); + parameters.Add("UserParam", $"%{userFilter}%"); + } + if (!string.IsNullOrWhiteSpace(nombreFilter)) + { + sqlBuilder.Append(" AND (u.Nombre LIKE @NombreParam OR u.Apellido LIKE @NombreParam)"); + parameters.Add("NombreParam", $"%{nombreFilter}%"); + } + sqlBuilder.Append(" ORDER BY u.[User];"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync( + sqlBuilder.ToString(), + (usuario, nombrePerfil) => (usuario, nombrePerfil), + parameters, + splitOn: "NombrePerfil" + ); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Usuarios con nombre de perfil."); + return Enumerable.Empty<(Usuario, string)>(); + } + } + + + public async Task GetByIdAsync(int id) + { + const string sql = "SELECT * FROM dbo.gral_Usuarios WHERE Id = @Id"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Usuario por ID: {UsuarioId}", id); + return null; + } + } + public async Task<(Usuario? Usuario, string? NombrePerfil)> GetByIdWithProfileNameAsync(int id) + { + const string sql = @" + SELECT u.*, p.perfil AS NombrePerfil + FROM dbo.gral_Usuarios u + INNER JOIN dbo.gral_Perfiles p ON u.IdPerfil = p.id + WHERE u.Id = @Id"; + try + { + using var connection = _connectionFactory.CreateConnection(); + var result = await connection.QueryAsync( + sql, + (usuario, nombrePerfil) => (usuario, nombrePerfil), + new { Id = id }, + splitOn: "NombrePerfil" + ); + return result.SingleOrDefault(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Usuario por ID con nombre de perfil: {UsuarioId}", id); + return (null, null); + } + } + + + public async Task GetByUsernameAsync(string username) + { + // Esta es la misma que en AuthRepository, si se unifican, se puede eliminar una. + const string sql = "SELECT * FROM dbo.gral_Usuarios WHERE [User] = @Username"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { Username = username }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Usuario por Username: {Username}", username); + return null; + } + } + + public async Task UserExistsAsync(string username, int? excludeId = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.gral_Usuarios WHERE [User] = @Username"); + var parameters = new DynamicParameters(); + parameters.Add("Username", username); + + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND Id != @ExcludeId"); + parameters.Add("ExcludeId", excludeId.Value); + } + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en UserExistsAsync para username: {Username}", username); + return true; // Asumir que existe para prevenir duplicados + } + } + + public async Task CreateAsync(Usuario nuevoUsuario, int idUsuarioCreador, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.gral_Usuarios ([User], ClaveHash, ClaveSalt, Habilitada, SupAdmin, Nombre, Apellido, IdPerfil, VerLog, DebeCambiarClave) + OUTPUT INSERTED.* + VALUES (@User, @ClaveHash, @ClaveSalt, @Habilitada, @SupAdmin, @Nombre, @Apellido, @IdPerfil, @VerLog, @DebeCambiarClave);"; + + const string sqlInsertHistorico = @" + INSERT INTO dbo.gral_Usuarios_H + (IdUsuario, UserNvo, HabilitadaNva, SupAdminNvo, NombreNvo, ApellidoNvo, IdPerfilNvo, DebeCambiarClaveNva, Id_UsuarioMod, FechaMod, TipoMod) + VALUES (@IdUsuarioHist, @UserNvoHist, @HabilitadaNvaHist, @SupAdminNvoHist, @NombreNvoHist, @ApellidoNvoHist, @IdPerfilNvoHist, @DebeCambiarClaveNvaHist, @IdUsuarioModHist, @FechaModHist, @TipoModHist);"; + + var connection = transaction.Connection!; + var insertedUsuario = await connection.QuerySingleAsync(sqlInsert, nuevoUsuario, transaction); + + if (insertedUsuario == null) throw new DataException("No se pudo crear el usuario."); + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdUsuarioHist = insertedUsuario.Id, + UserNvoHist = insertedUsuario.User, + HabilitadaNvaHist = insertedUsuario.Habilitada, + SupAdminNvoHist = insertedUsuario.SupAdmin, + NombreNvoHist = insertedUsuario.Nombre, + ApellidoNvoHist = insertedUsuario.Apellido, + IdPerfilNvoHist = insertedUsuario.IdPerfil, + DebeCambiarClaveNvaHist = insertedUsuario.DebeCambiarClave, + IdUsuarioModHist = idUsuarioCreador, + FechaModHist = DateTime.Now, + TipoModHist = "Creado" + }, transaction); + + return insertedUsuario; + } + + public async Task UpdateAsync(Usuario usuarioAActualizar, int idUsuarioModificador, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var usuarioActual = await connection.QuerySingleOrDefaultAsync( + "SELECT * FROM dbo.gral_Usuarios WHERE Id = @Id", new { usuarioAActualizar.Id }, transaction); + + if (usuarioActual == null) throw new KeyNotFoundException("Usuario no encontrado para actualizar."); + + // User (nombre de usuario) no se actualiza aquí + const string sqlUpdate = @" + UPDATE dbo.gral_Usuarios SET + Habilitada = @Habilitada, SupAdmin = @SupAdmin, Nombre = @Nombre, Apellido = @Apellido, + IdPerfil = @IdPerfil, VerLog = @VerLog, DebeCambiarClave = @DebeCambiarClave + WHERE Id = @Id;"; + + const string sqlInsertHistorico = @" + INSERT INTO dbo.gral_Usuarios_H + (IdUsuario, UserAnt, UserNvo, HabilitadaAnt, HabilitadaNva, SupAdminAnt, SupAdminNvo, + NombreAnt, NombreNvo, ApellidoAnt, ApellidoNvo, IdPerfilAnt, IdPerfilNvo, + DebeCambiarClaveAnt, DebeCambiarClaveNva, Id_UsuarioMod, FechaMod, TipoMod) + VALUES (@IdUsuarioHist, @UserAntHist, @UserNvoHist, @HabilitadaAntHist, @HabilitadaNvaHist, @SupAdminAntHist, @SupAdminNvoHist, + @NombreAntHist, @NombreNvoHist, @ApellidoAntHist, @ApellidoNvoHist, @IdPerfilAntHist, @IdPerfilNvoHist, + @DebeCambiarClaveAntHist, @DebeCambiarClaveNvaHist, @IdUsuarioModHist, @FechaModHist, @TipoModHist);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdUsuarioHist = usuarioActual.Id, + UserAntHist = usuarioActual.User, UserNvoHist = usuarioAActualizar.User, // Aunque no cambiemos User, lo registramos + HabilitadaAntHist = usuarioActual.Habilitada, HabilitadaNvaHist = usuarioAActualizar.Habilitada, + SupAdminAntHist = usuarioActual.SupAdmin, SupAdminNvoHist = usuarioAActualizar.SupAdmin, + NombreAntHist = usuarioActual.Nombre, NombreNvoHist = usuarioAActualizar.Nombre, + ApellidoAntHist = usuarioActual.Apellido, ApellidoNvoHist = usuarioAActualizar.Apellido, + IdPerfilAntHist = usuarioActual.IdPerfil, IdPerfilNvoHist = usuarioAActualizar.IdPerfil, + DebeCambiarClaveAntHist = usuarioActual.DebeCambiarClave, DebeCambiarClaveNvaHist = usuarioAActualizar.DebeCambiarClave, + IdUsuarioModHist = idUsuarioModificador, + FechaModHist = DateTime.Now, + TipoModHist = "Actualizado" + }, transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, usuarioAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task SetPasswordAsync(int userId, string newHash, string newSalt, bool debeCambiarClave, int idUsuarioModificador, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var usuarioActual = await connection.QuerySingleOrDefaultAsync( + "SELECT * FROM dbo.gral_Usuarios WHERE Id = @Id", new { Id = userId }, transaction); + + if (usuarioActual == null) throw new KeyNotFoundException("Usuario no encontrado para cambiar contraseña."); + + const string sqlUpdate = @"UPDATE dbo.gral_Usuarios + SET ClaveHash = @ClaveHash, ClaveSalt = @ClaveSalt, DebeCambiarClave = @DebeCambiarClave + WHERE Id = @UserId"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.gral_Usuarios_H + (IdUsuario, UserNvo, HabilitadaNva, SupAdminNvo, NombreNvo, ApellidoNvo, IdPerfilNvo, + DebeCambiarClaveAnt, DebeCambiarClaveNva, Id_UsuarioMod, FechaMod, TipoMod) + VALUES (@IdUsuarioHist, @UserNvoHist, @HabilitadaNvaHist, @SupAdminNvoHist, @NombreNvoHist, @ApellidoNvoHist, @IdPerfilNvoHist, + @DebeCambiarClaveAntHist, @DebeCambiarClaveNvaHist, @IdUsuarioModHist, @FechaModHist, @TipoModHist);"; + + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdUsuarioHist = usuarioActual.Id, + UserNvoHist = usuarioActual.User, // No cambia el user + HabilitadaNvaHist = usuarioActual.Habilitada, + SupAdminNvoHist = usuarioActual.SupAdmin, + NombreNvoHist = usuarioActual.Nombre, + ApellidoNvoHist = usuarioActual.Apellido, + IdPerfilNvoHist = usuarioActual.IdPerfil, + DebeCambiarClaveAntHist = usuarioActual.DebeCambiarClave, // Estado anterior de este flag + DebeCambiarClaveNvaHist = debeCambiarClave, // Nuevo estado de este flag + IdUsuarioModHist = idUsuarioModificador, + FechaModHist = DateTime.Now, + TipoModHist = "Clave Cambiada" // O "Clave Reseteada" + }, transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new + { + ClaveHash = newHash, + ClaveSalt = newSalt, + DebeCambiarClave = debeCambiarClave, + UserId = userId + }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/Canilla.cs b/Backend/GestionIntegral.Api/Models/Distribucion/Canilla.cs new file mode 100644 index 0000000..460ccb1 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/Canilla.cs @@ -0,0 +1,16 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class Canilla + { + public int IdCanilla { get; set; } // Id_Canilla (PK, Identity) + public int? Legajo { get; set; } // Legajo (int, NULL) + public string NomApe { get; set; } = string.Empty; // NomApe (varchar(100), NOT NULL) + public string? Parada { get; set; } // Parada (varchar(150), NULL) + public int IdZona { get; set; } // Id_Zona (int, NOT NULL) + public bool Accionista { get; set; } // Accionista (bit, NOT NULL) + public string? Obs { get; set; } // Obs (varchar(150), NULL) + public int Empresa { get; set; } // Empresa (int, NOT NULL, DEFAULT 0) + public bool Baja { get; set; } // Baja (bit, NOT NULL, DEFAULT 0) + public DateTime? FechaBaja { get; set; } // FechaBaja (datetime2(0), NULL) + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/CanillaHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/CanillaHistorico.cs new file mode 100644 index 0000000..fcf2ee0 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/CanillaHistorico.cs @@ -0,0 +1,21 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class CanillaHistorico + { + public int IdCanilla { get; set; } + public int? Legajo { get; set; } + public string NomApe { get; set; } = string.Empty; + public string? Parada { get; set; } + public int IdZona { get; set; } + public bool Accionista { get; set; } + public string? Obs { get; set; } + public int Empresa { get; set; } + public bool Baja { get; set; } + public DateTime? FechaBaja { get; set; } + + // Campos de Auditoría + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/Distribuidor.cs b/Backend/GestionIntegral.Api/Models/Distribucion/Distribuidor.cs new file mode 100644 index 0000000..2a5dca7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/Distribuidor.cs @@ -0,0 +1,18 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class Distribuidor + { + public int IdDistribuidor { get; set; } // Id_Distribuidor (PK, Identity) + public string Nombre { get; set; } = string.Empty; // Nombre (varchar(100), NOT NULL) + public string? Contacto { get; set; } // Contacto (varchar(100), NULL) + public string NroDoc { get; set; } = string.Empty; // NroDoc (varchar(11), NOT NULL) + public int? IdZona { get; set; } // Id_Zona (int, NULL) - Puede no tener zona asignada + public string? Calle { get; set; } + public string? Numero { get; set; } + public string? Piso { get; set; } + public string? Depto { get; set; } + public string? Telefono { get; set; } + public string? Email { get; set; } + public string? Localidad { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/DistribuidorHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/DistribuidorHistorico.cs new file mode 100644 index 0000000..0af650c --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/DistribuidorHistorico.cs @@ -0,0 +1,23 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class DistribuidorHistorico + { + public int IdDistribuidor { get; set; } // FK + public string Nombre { get; set; } = string.Empty; + public string? Contacto { get; set; } + public string NroDoc { get; set; } = string.Empty; + public int? IdZona { get; set; } + public string? Calle { get; set; } + public string? Numero { get; set; } + public string? Piso { get; set; } + public string? Depto { get; set; } + public string? Telefono { get; set; } + public string? Email { get; set; } + public string? Localidad { get; set; } + + // Campos de Auditoría + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Empresas/EmpresaHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/EmpresaHistorico.cs similarity index 100% rename from Backend/GestionIntegral.Api/Models/Empresas/EmpresaHistorico.cs rename to Backend/GestionIntegral.Api/Models/Distribucion/EmpresaHistorico.cs diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/OtroDestino.cs b/Backend/GestionIntegral.Api/Models/Distribucion/OtroDestino.cs new file mode 100644 index 0000000..c02b9f0 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/OtroDestino.cs @@ -0,0 +1,14 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class OtroDestino + { + // Columna: Id_Destino (PK, Identity) + public int IdDestino { get; set; } + + // Columna: Nombre (varchar(100), NOT NULL) + public string Nombre { get; set; } = string.Empty; + + // Columna: Obs (varchar(250), NULL) + public string? Obs { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/OtroDestinoHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/OtroDestinoHistorico.cs new file mode 100644 index 0000000..3d60676 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/OtroDestinoHistorico.cs @@ -0,0 +1,19 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class OtroDestinoHistorico + { + // Columna: Id_Destino (int, FK) + public int IdDestino { get; set; } + + // Columna: Nombre (varchar(100), NOT NULL) + public string Nombre { get; set; } = string.Empty; + + // Columna: Obs (varchar(250), NULL) + public string? Obs { get; set; } + + // Columnas de Auditoría + public int IdUsuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; // "Insertada", "Modificada", "Eliminada" + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/PorcMonCanilla.cs b/Backend/GestionIntegral.Api/Models/Distribucion/PorcMonCanilla.cs new file mode 100644 index 0000000..2e0c270 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/PorcMonCanilla.cs @@ -0,0 +1,15 @@ +using System; + +namespace GestionIntegral.Api.Models.Distribucion +{ + public class PorcMonCanilla // Corresponde a dist_PorcMonPagoCanilla + { + public int IdPorcMon { get; set; } // Id_PorcMon + public int IdPublicacion { get; set; } + public int IdCanilla { get; set; } + public DateTime VigenciaD { get; set; } + public DateTime? VigenciaH { get; set; } + public decimal PorcMon { get; set; } // Columna PorcMon en DB + public bool EsPorcentaje { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/PorcMonCanillaHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/PorcMonCanillaHistorico.cs new file mode 100644 index 0000000..6a0389c --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/PorcMonCanillaHistorico.cs @@ -0,0 +1,18 @@ +using System; + +namespace GestionIntegral.Api.Models.Distribucion +{ + public class PorcMonCanillaHistorico + { + public int Id_PorcMon { get; set; } // Coincide con la columna + public int Id_Publicacion { get; set; } + public int Id_Canilla { get; set; } + public DateTime VigenciaD { get; set; } + public DateTime? VigenciaH { get; set; } + public decimal PorcMon { get; set; } + public bool EsPorcentaje { get; set; } + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/PorcPago.cs b/Backend/GestionIntegral.Api/Models/Distribucion/PorcPago.cs new file mode 100644 index 0000000..638a0c9 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/PorcPago.cs @@ -0,0 +1,14 @@ +using System; + +namespace GestionIntegral.Api.Models.Distribucion +{ + public class PorcPago // Corresponde a dist_PorcPago + { + public int IdPorcentaje { get; set; } // Id_Porcentaje + public int IdPublicacion { get; set; } + public int IdDistribuidor { get; set; } + public DateTime VigenciaD { get; set; } // smalldatetime se mapea a DateTime + public DateTime? VigenciaH { get; set; } + public decimal Porcentaje { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/PorcPagoHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/PorcPagoHistorico.cs new file mode 100644 index 0000000..8568b13 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/PorcPagoHistorico.cs @@ -0,0 +1,16 @@ +using System; +namespace GestionIntegral.Api.Models.Distribucion +{ + public class PorcPagoHistorico + { + public int IdPorcentaje { get; set; } // Id_Porcentaje + public int IdPublicacion { get; set; } + public int IdDistribuidor { get; set; } + public DateTime VigenciaD { get; set; } + public DateTime? VigenciaH { get; set; } + public decimal Porcentaje { get; set; } + public int IdUsuario { get; set; } // Coincide con Id_Usuario en la tabla _H + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/Precio.cs b/Backend/GestionIntegral.Api/Models/Distribucion/Precio.cs new file mode 100644 index 0000000..b3328c3 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/Precio.cs @@ -0,0 +1,17 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class Precio + { + public int IdPrecio { get; set; } // Id_Precio (PK, Identity) + public int IdPublicacion { get; set; } // Id_Publicacion (FK, NOT NULL) + public DateTime VigenciaD { get; set; } // VigenciaD (datetime2(0), NOT NULL) + public DateTime? VigenciaH { get; set; } // VigenciaH (datetime2(0), NULL) + public decimal? Lunes { get; set; } // Lunes (decimal(14,2), NULL, DEFAULT 0) + public decimal? Martes { get; set; } + public decimal? Miercoles { get; set; } + public decimal? Jueves { get; set; } + public decimal? Viernes { get; set; } + public decimal? Sabado { get; set; } + public decimal? Domingo { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/PrecioHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/PrecioHistorico.cs new file mode 100644 index 0000000..df30dc0 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/PrecioHistorico.cs @@ -0,0 +1,22 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class PrecioHistorico + { + public int IdPrecio { get; set; } // FK a dist_Precios.Id_Precio + public int IdPublicacion { get; set; } + public DateTime VigenciaD { get; set; } + public DateTime? VigenciaH { get; set; } + public decimal? Lunes { get; set; } + public decimal? Martes { get; set; } + public decimal? Miercoles { get; set; } + public decimal? Jueves { get; set; } + public decimal? Viernes { get; set; } + public decimal? Sabado { get; set; } + public decimal? Domingo { get; set; } + + // Campos de Auditoría + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/PubliSeccion.cs b/Backend/GestionIntegral.Api/Models/Distribucion/PubliSeccion.cs new file mode 100644 index 0000000..530bda2 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/PubliSeccion.cs @@ -0,0 +1,10 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class PubliSeccion + { + public int IdSeccion { get; set; } // Id_Seccion + public int IdPublicacion { get; set; } + public string Nombre { get; set; } = string.Empty; + public bool Estado { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/PubliSeccionHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/PubliSeccionHistorico.cs new file mode 100644 index 0000000..08d8932 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/PubliSeccionHistorico.cs @@ -0,0 +1,15 @@ +using System; + +namespace GestionIntegral.Api.Models.Distribucion +{ + public class PubliSeccionHistorico + { + public int Id_Seccion { get; set; } // Coincide con la columna + public int Id_Publicacion { get; set; } + public string Nombre { get; set; } = string.Empty; + public bool Estado { get; set; } + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/Publicacion.cs b/Backend/GestionIntegral.Api/Models/Distribucion/Publicacion.cs new file mode 100644 index 0000000..2bf036f --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/Publicacion.cs @@ -0,0 +1,12 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class Publicacion + { + public int IdPublicacion { get; set; } // Id_Publicacion (PK, Identity) + public string Nombre { get; set; } = string.Empty; // Nombre (varchar(50), NOT NULL) + public string? Observacion { get; set; } // Observacion (varchar(150), NULL) + public int IdEmpresa { get; set; } // Id_Empresa (int, NOT NULL) + public bool CtrlDevoluciones { get; set; } // CtrlDevoluciones (bit, NOT NULL, DEFAULT 0) + public bool? Habilitada { get; set; } // Habilitada (bit, NULL, DEFAULT 1) - ¡OJO! En la tabla es NULLABLE con DEFAULT 1 + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/PublicacionHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/PublicacionHistorico.cs new file mode 100644 index 0000000..e50a374 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/PublicacionHistorico.cs @@ -0,0 +1,17 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class PublicacionHistorico + { + // No hay PK autoincremental explícita en el script de _H + public int IdPublicacion { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Observacion { get; set; } + public int IdEmpresa { get; set; } + public bool? Habilitada { get; set; } // Coincide con la tabla _H + + // Campos de Auditoría + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/RecargoZona.cs b/Backend/GestionIntegral.Api/Models/Distribucion/RecargoZona.cs new file mode 100644 index 0000000..6636ae7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/RecargoZona.cs @@ -0,0 +1,14 @@ +using System; + +namespace GestionIntegral.Api.Models.Distribucion +{ + public class RecargoZona + { + public int IdRecargo { get; set; } // Id_Recargo (PK, Identity) + public int IdPublicacion { get; set; } // Id_Publicacion (FK, NOT NULL) + public int IdZona { get; set; } // Id_Zona (FK, NOT NULL) + public DateTime VigenciaD { get; set; } // VigenciaD (datetime2(0), NOT NULL) + public DateTime? VigenciaH { get; set; } // VigenciaH (datetime2(0), NULL) + public decimal Valor { get; set; } // Valor (decimal(14,2), NOT NULL, DEFAULT 0) + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/RecargoZonaHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/RecargoZonaHistorico.cs new file mode 100644 index 0000000..5435da1 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/RecargoZonaHistorico.cs @@ -0,0 +1,15 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class RecargoZonaHistorico + { + public int IdRecargo { get; set; } + public int IdPublicacion { get; set; } + public int IdZona { get; set; } + public DateTime VigenciaD { get; set; } + public DateTime? VigenciaH { get; set; } + public decimal Valor { get; set; } + public int IdUsuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CanillaDto.cs new file mode 100644 index 0000000..0effab8 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CanillaDto.cs @@ -0,0 +1,18 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CanillaDto + { + public int IdCanilla { get; set; } + public int? Legajo { get; set; } + public string NomApe { get; set; } = string.Empty; + public string? Parada { get; set; } + public int IdZona { get; set; } + public string NombreZona { get; set; } = string.Empty; // Para mostrar en la UI + public bool Accionista { get; set; } + public string? Obs { get; set; } + public int Empresa { get; set; } // Podría traer el nombre de la empresa si es relevante + public string NombreEmpresa { get; set;} = string.Empty; + public bool Baja { get; set; } + public string? FechaBaja { get; set; } // Formato string para la UI + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateCanillaDto.cs new file mode 100644 index 0000000..9b1be6a --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateCanillaDto.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreateCanillaDto + { + public int? Legajo { get; set; } + + [Required(ErrorMessage = "El nombre y apellido son obligatorios.")] + [StringLength(100)] + public string NomApe { get; set; } = string.Empty; + + [StringLength(150)] + public string? Parada { get; set; } + + [Required(ErrorMessage = "La zona es obligatoria.")] + [Range(1, int.MaxValue, ErrorMessage = "Debe seleccionar una zona válida.")] + public int IdZona { get; set; } + + [Required] + public bool Accionista { get; set; } = false; + + [StringLength(150)] + public string? Obs { get; set; } + + [Required(ErrorMessage = "La empresa es obligatoria.")] + // Asumimos que Empresa 0 es válido para accionistas según el contexto. + // Si Empresa 0 es un placeholder, entonces Range(1, int.MaxValue) + public int Empresa { get; set; } = 0; // Default 0 según la tabla + + // Baja y FechaBaja se manejan por una acción separada, no en creación. + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateDistribuidorDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateDistribuidorDto.cs new file mode 100644 index 0000000..78abc04 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateDistribuidorDto.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreateDistribuidorDto + { + [Required(ErrorMessage = "El nombre del distribuidor es obligatorio.")] + [StringLength(100)] + public string Nombre { get; set; } = string.Empty; + + [StringLength(100)] + public string? Contacto { get; set; } + + [Required(ErrorMessage = "El número de documento es obligatorio.")] + [StringLength(11)] // CUIT/CUIL suele tener 11 dígitos + public string NroDoc { get; set; } = string.Empty; + + public int? IdZona { get; set; } // Puede ser nulo si el distribuidor no tiene zona + + [StringLength(30)] + public string? Calle { get; set; } + [StringLength(8)] + public string? Numero { get; set; } + [StringLength(3)] + public string? Piso { get; set; } + [StringLength(4)] + public string? Depto { get; set; } + [StringLength(40)] + public string? Telefono { get; set; } + [StringLength(40)] + [EmailAddress(ErrorMessage = "Formato de email inválido.")] + public string? Email { get; set; } + [StringLength(50)] + public string? Localidad { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Empresas/CreateEmpresaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateEmpresaDto.cs similarity index 100% rename from Backend/GestionIntegral.Api/Models/Dtos/Empresas/CreateEmpresaDto.cs rename to Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateEmpresaDto.cs diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateOtroDestinoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateOtroDestinoDto.cs new file mode 100644 index 0000000..0c81f16 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateOtroDestinoDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreateOtroDestinoDto + { + [Required(ErrorMessage = "El nombre del destino es obligatorio.")] + [StringLength(100, ErrorMessage = "El nombre no puede exceder los 100 caracteres.")] + public string Nombre { get; set; } = string.Empty; + + [StringLength(250, ErrorMessage = "La observación no puede exceder los 250 caracteres.")] + public string? Obs { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePrecioDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePrecioDto.cs new file mode 100644 index 0000000..136523e --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePrecioDto.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreatePrecioDto + { + [Required(ErrorMessage = "El ID de la publicación es obligatorio.")] + public int IdPublicacion { get; set; } + + [Required(ErrorMessage = "La fecha de Vigencia Desde es obligatoria.")] + public DateTime VigenciaD { get; set; } // Recibir como DateTime desde el cliente + + // VigenciaH se calculará o se dejará null inicialmente. + + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Lunes { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Martes { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Miercoles { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Jueves { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Viernes { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Sabado { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Domingo { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePublicacionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePublicacionDto.cs new file mode 100644 index 0000000..3fb29ae --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreatePublicacionDto.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreatePublicacionDto + { + [Required(ErrorMessage = "El nombre de la publicación es obligatorio.")] + [StringLength(50)] + public string Nombre { get; set; } = string.Empty; + + [StringLength(150)] + public string? Observacion { get; set; } + + [Required(ErrorMessage = "La empresa es obligatoria.")] + [Range(1, int.MaxValue, ErrorMessage = "Debe seleccionar una empresa válida.")] + public int IdEmpresa { get; set; } + + [Required] + public bool CtrlDevoluciones { get; set; } = false; + + public bool Habilitada { get; set; } = true; // Default true en UI + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Zonas/CreateZonaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateZonaDto.cs similarity index 100% rename from Backend/GestionIntegral.Api/Models/Dtos/Zonas/CreateZonaDto.cs rename to Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateZonaDto.cs diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/DistribuidorDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/DistribuidorDto.cs new file mode 100644 index 0000000..b415a14 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/DistribuidorDto.cs @@ -0,0 +1,19 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class DistribuidorDto + { + public int IdDistribuidor { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Contacto { get; set; } + public string NroDoc { get; set; } = string.Empty; + public int? IdZona { get; set; } + public string? NombreZona { get; set; } // Para mostrar en UI + public string? Calle { get; set; } + public string? Numero { get; set; } + public string? Piso { get; set; } + public string? Depto { get; set; } + public string? Telefono { get; set; } + public string? Email { get; set; } + public string? Localidad { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Empresas/EmpresaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EmpresaDto.cs similarity index 100% rename from Backend/GestionIntegral.Api/Models/Dtos/Empresas/EmpresaDto.cs rename to Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EmpresaDto.cs diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/OtroDestinoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/OtroDestinoDto.cs new file mode 100644 index 0000000..6296fea --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/OtroDestinoDto.cs @@ -0,0 +1,9 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class OtroDestinoDto + { + public int IdDestino { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Obs { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PrecioDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PrecioDto.cs new file mode 100644 index 0000000..d512ddf --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PrecioDto.cs @@ -0,0 +1,17 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class PrecioDto + { + public int IdPrecio { get; set; } + public int IdPublicacion { get; set; } + public string VigenciaD { get; set; } = string.Empty; // string yyyy-MM-dd para la UI + public string? VigenciaH { get; set; } // string yyyy-MM-dd para la UI + public decimal? Lunes { get; set; } + public decimal? Martes { get; set; } + public decimal? Miercoles { get; set; } + public decimal? Jueves { get; set; } + public decimal? Viernes { get; set; } + public decimal? Sabado { get; set; } + public decimal? Domingo { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDto.cs new file mode 100644 index 0000000..f1ca7b8 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/PublicacionDto.cs @@ -0,0 +1,13 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class PublicacionDto + { + public int IdPublicacion { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Observacion { get; set; } + public int IdEmpresa { get; set; } + public string NombreEmpresa { get; set; } = string.Empty; // Para mostrar en UI + public bool CtrlDevoluciones { get; set; } + public bool Habilitada { get; set; } // Simplificamos a bool, el backend manejará el default si es null + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/ToggleBajaCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/ToggleBajaCanillaDto.cs new file mode 100644 index 0000000..063c955 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/ToggleBajaCanillaDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class ToggleBajaCanillaDto + { + [Required] + public bool DarDeBaja { get; set; } // True para dar de baja, False para reactivar + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateCanillaDto.cs new file mode 100644 index 0000000..cbca341 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateCanillaDto.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdateCanillaDto + { + public int? Legajo { get; set; } + + [Required(ErrorMessage = "El nombre y apellido son obligatorios.")] + [StringLength(100)] + public string NomApe { get; set; } = string.Empty; + + [StringLength(150)] + public string? Parada { get; set; } + + [Required(ErrorMessage = "La zona es obligatoria.")] + [Range(1, int.MaxValue, ErrorMessage = "Debe seleccionar una zona válida.")] + public int IdZona { get; set; } + + [Required] + public bool Accionista { get; set; } + + [StringLength(150)] + public string? Obs { get; set; } + + [Required(ErrorMessage = "La empresa es obligatoria.")] + public int Empresa { get; set; } + + // Baja y FechaBaja se manejan por una acción separada. + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateDistribuidorDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateDistribuidorDto.cs new file mode 100644 index 0000000..807ebb2 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateDistribuidorDto.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdateDistribuidorDto + { + [Required(ErrorMessage = "El nombre del distribuidor es obligatorio.")] + [StringLength(100)] + public string Nombre { get; set; } = string.Empty; + + [StringLength(100)] + public string? Contacto { get; set; } + + [Required(ErrorMessage = "El número de documento es obligatorio.")] + [StringLength(11)] + public string NroDoc { get; set; } = string.Empty; + + public int? IdZona { get; set; } + + [StringLength(30)] + public string? Calle { get; set; } + [StringLength(8)] + public string? Numero { get; set; } + [StringLength(3)] + public string? Piso { get; set; } + [StringLength(4)] + public string? Depto { get; set; } + [StringLength(40)] + public string? Telefono { get; set; } + [StringLength(40)] + [EmailAddress(ErrorMessage = "Formato de email inválido.")] + public string? Email { get; set; } + [StringLength(50)] + public string? Localidad { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Empresas/UpdateEmpresaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateEmpresaDto.cs similarity index 100% rename from Backend/GestionIntegral.Api/Models/Dtos/Empresas/UpdateEmpresaDto.cs rename to Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateEmpresaDto.cs diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateOtroDestinoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateOtroDestinoDto.cs new file mode 100644 index 0000000..47361d6 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateOtroDestinoDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdateOtroDestinoDto + { + [Required(ErrorMessage = "El nombre del destino es obligatorio.")] + [StringLength(100, ErrorMessage = "El nombre no puede exceder los 100 caracteres.")] + public string Nombre { get; set; } = string.Empty; + + [StringLength(250, ErrorMessage = "La observación no puede exceder los 250 caracteres.")] + public string? Obs { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePrecioDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePrecioDto.cs new file mode 100644 index 0000000..24364a9 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePrecioDto.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdatePrecioDto // Para actualizar un IdPrecio específico + { + // IdPublicacion no se puede cambiar una vez creado el precio (se borraría y crearía uno nuevo) + // VigenciaD tampoco se debería cambiar directamente, se maneja creando nuevos periodos. + + [Required(ErrorMessage = "La fecha de Vigencia Hasta es obligatoria si se modifica un periodo existente para cerrarlo.")] + public DateTime? VigenciaH { get; set; } // Para cerrar un periodo + + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Lunes { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Martes { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Miercoles { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Jueves { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Viernes { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Sabado { get; set; } + [Range(0, 999999.99, ErrorMessage = "El precio debe ser un valor positivo.")] + public decimal? Domingo { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePublicacionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePublicacionDto.cs new file mode 100644 index 0000000..f48f6b6 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdatePublicacionDto.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdatePublicacionDto + { + [Required(ErrorMessage = "El nombre de la publicación es obligatorio.")] + [StringLength(50)] + public string Nombre { get; set; } = string.Empty; + + [StringLength(150)] + public string? Observacion { get; set; } + + [Required(ErrorMessage = "La empresa es obligatoria.")] + [Range(1, int.MaxValue, ErrorMessage = "Debe seleccionar una empresa válida.")] + public int IdEmpresa { get; set; } + + [Required] + public bool CtrlDevoluciones { get; set; } + + [Required] // En la actualización, el estado de Habilitada debe ser explícito + public bool Habilitada { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Zonas/UpdateZonaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateZonaDto.cs similarity index 100% rename from Backend/GestionIntegral.Api/Models/Dtos/Zonas/UpdateZonaDto.cs rename to Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateZonaDto.cs diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Zonas/ZonaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/ZonaDto.cs similarity index 100% rename from Backend/GestionIntegral.Api/Models/Dtos/Zonas/ZonaDto.cs rename to Backend/GestionIntegral.Api/Models/Dtos/Distribucion/ZonaDto.cs diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/ActualizarPermisosPerfilRequestDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/ActualizarPermisosPerfilRequestDto.cs new file mode 100644 index 0000000..f652d6c --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/ActualizarPermisosPerfilRequestDto.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class ActualizarPermisosPerfilRequestDto + { + [Required] + [MinLength(0)] // Puede que un perfil no tenga ningún permiso. + public List PermisosIds { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/ChangePasswordRequestDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/ChangePasswordRequestDto.cs similarity index 100% rename from Backend/GestionIntegral.Api/Models/Dtos/ChangePasswordRequestDto.cs rename to Backend/GestionIntegral.Api/Models/Dtos/Usuarios/ChangePasswordRequestDto.cs diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreatePerfilDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreatePerfilDto.cs new file mode 100644 index 0000000..4d3eec2 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreatePerfilDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class CreatePerfilDto + { + [Required(ErrorMessage = "El nombre del perfil es obligatorio.")] + [StringLength(20, ErrorMessage = "El nombre del perfil no puede exceder los 20 caracteres.")] + public string NombrePerfil { get; set; } = string.Empty; + + [StringLength(150, ErrorMessage = "La descripción no puede exceder los 150 caracteres.")] + public string? Descripcion { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreatePermisoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreatePermisoDto.cs new file mode 100644 index 0000000..9d62aab --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreatePermisoDto.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class CreatePermisoDto + { + [Required(ErrorMessage = "El módulo es obligatorio.")] + [StringLength(50, ErrorMessage = "El módulo no puede exceder los 50 caracteres.")] + public string Modulo { get; set; } = string.Empty; + + [Required(ErrorMessage = "La descripción del permiso es obligatoria.")] + [StringLength(150, ErrorMessage = "La descripción no puede exceder los 150 caracteres.")] + public string DescPermiso { get; set; } = string.Empty; + + [Required(ErrorMessage = "El código de acceso es obligatorio.")] + [StringLength(10, ErrorMessage = "El código de acceso no puede exceder los 10 caracteres.")] + // Podrías añadir una RegularExpression si los codAcc tienen un formato específico + public string CodAcc { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreateUsuarioRequestDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreateUsuarioRequestDto.cs new file mode 100644 index 0000000..76ae707 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/CreateUsuarioRequestDto.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class CreateUsuarioRequestDto + { + [Required(ErrorMessage = "El nombre de usuario es obligatorio.")] + [StringLength(20, MinimumLength = 3, ErrorMessage = "El nombre de usuario debe tener entre 3 y 20 caracteres.")] + public string User { get; set; } = string.Empty; + + [Required(ErrorMessage = "La contraseña es obligatoria.")] + [StringLength(50, MinimumLength = 6, ErrorMessage = "La contraseña debe tener al menos 6 caracteres.")] + public string Password { get; set; } = string.Empty; // Contraseña en texto plano para la creación + + [Required(ErrorMessage = "El nombre es obligatorio.")] + [StringLength(50)] + public string Nombre { get; set; } = string.Empty; + + [Required(ErrorMessage = "El apellido es obligatorio.")] + [StringLength(50)] + public string Apellido { get; set; } = string.Empty; + + [Required(ErrorMessage = "El perfil es obligatorio.")] + [Range(1, int.MaxValue, ErrorMessage = "Debe seleccionar un perfil válido.")] + public int IdPerfil { get; set; } + + public bool Habilitada { get; set; } = true; + public bool SupAdmin { get; set; } = false; + public bool DebeCambiarClave { get; set; } = true; // Por defecto, forzar cambio al primer login + + [StringLength(10)] + public string VerLog { get; set; } = "1.0.0.0"; // Valor por defecto + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/LoginRequestDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/LoginRequestDto.cs similarity index 100% rename from Backend/GestionIntegral.Api/Models/Dtos/LoginRequestDto.cs rename to Backend/GestionIntegral.Api/Models/Dtos/Usuarios/LoginRequestDto.cs diff --git a/Backend/GestionIntegral.Api/Models/Dtos/LoginResponseDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/LoginResponseDto.cs similarity index 100% rename from Backend/GestionIntegral.Api/Models/Dtos/LoginResponseDto.cs rename to Backend/GestionIntegral.Api/Models/Dtos/Usuarios/LoginResponseDto.cs diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PerfilDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PerfilDto.cs new file mode 100644 index 0000000..8d77a04 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PerfilDto.cs @@ -0,0 +1,9 @@ +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class PerfilDto + { + public int Id { get; set; } + public string NombrePerfil { get; set; } = string.Empty; + public string? Descripcion { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PermisoAsignadoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PermisoAsignadoDto.cs new file mode 100644 index 0000000..fcb265a --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PermisoAsignadoDto.cs @@ -0,0 +1,11 @@ +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class PermisoAsignadoDto + { + public int Id { get; set; } + public string Modulo { get; set; } = string.Empty; + public string DescPermiso { get; set; } = string.Empty; + public string CodAcc { get; set; } = string.Empty; + public bool Asignado { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PermisoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PermisoDto.cs new file mode 100644 index 0000000..da4c916 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/PermisoDto.cs @@ -0,0 +1,10 @@ +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class PermisoDto + { + public int Id { get; set; } + public string Modulo { get; set; } = string.Empty; + public string DescPermiso { get; set; } = string.Empty; + public string CodAcc { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/SetPasswordRequestDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/SetPasswordRequestDto.cs new file mode 100644 index 0000000..5d7521b --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/SetPasswordRequestDto.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class SetPasswordRequestDto + { + [Required] + [StringLength(50, MinimumLength = 6, ErrorMessage = "La nueva contraseña debe tener al menos 6 caracteres.")] + public string NewPassword { get; set; } = string.Empty; + public bool ForceChangeOnNextLogin { get; set; } = true; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdatePerfilDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdatePerfilDto.cs new file mode 100644 index 0000000..590a59a --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdatePerfilDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class UpdatePerfilDto + { + [Required(ErrorMessage = "El nombre del perfil es obligatorio.")] + [StringLength(20, ErrorMessage = "El nombre del perfil no puede exceder los 20 caracteres.")] + public string NombrePerfil { get; set; } = string.Empty; + + [StringLength(150, ErrorMessage = "La descripción no puede exceder los 150 caracteres.")] + public string? Descripcion { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdatePermisoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdatePermisoDto.cs new file mode 100644 index 0000000..bacf601 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdatePermisoDto.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class UpdatePermisoDto + { + [Required(ErrorMessage = "El módulo es obligatorio.")] + [StringLength(50, ErrorMessage = "El módulo no puede exceder los 50 caracteres.")] + public string Modulo { get; set; } = string.Empty; + + [Required(ErrorMessage = "La descripción del permiso es obligatoria.")] + [StringLength(150, ErrorMessage = "La descripción no puede exceder los 150 caracteres.")] + public string DescPermiso { get; set; } = string.Empty; + + [Required(ErrorMessage = "El código de acceso es obligatorio.")] + [StringLength(10, ErrorMessage = "El código de acceso no puede exceder los 10 caracteres.")] + public string CodAcc { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdateUsuarioRequestDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdateUsuarioRequestDto.cs new file mode 100644 index 0000000..914ec69 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UpdateUsuarioRequestDto.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class UpdateUsuarioRequestDto + { + // User no se puede cambiar usualmente, o requiere un proceso separado. + // Si se permite cambiar, añadir validaciones. Por ahora lo omitimos del DTO de update. + + [Required(ErrorMessage = "El nombre es obligatorio.")] + [StringLength(50)] + public string Nombre { get; set; } = string.Empty; + + [Required(ErrorMessage = "El apellido es obligatorio.")] + [StringLength(50)] + public string Apellido { get; set; } = string.Empty; + + [Required(ErrorMessage = "El perfil es obligatorio.")] + [Range(1, int.MaxValue, ErrorMessage = "Debe seleccionar un perfil válido.")] + public int IdPerfil { get; set; } + + [Required(ErrorMessage = "El estado Habilitada es obligatorio.")] + public bool Habilitada { get; set; } + + [Required(ErrorMessage = "El estado SupAdmin es obligatorio.")] + public bool SupAdmin { get; set; } + + [Required(ErrorMessage = "El estado DebeCambiarClave es obligatorio.")] + public bool DebeCambiarClave { get; set; } + + [StringLength(10)] + public string VerLog { get; set; } = "1.0.0.0"; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UsuarioDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UsuarioDto.cs new file mode 100644 index 0000000..5c30a92 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/UsuarioDto.cs @@ -0,0 +1,16 @@ +namespace GestionIntegral.Api.Dtos.Usuarios +{ + public class UsuarioDto // Para listar y ver detalles + { + public int Id { get; set; } + public string User { get; set; } = string.Empty; + public bool Habilitada { get; set; } + public bool SupAdmin { get; set; } + public string Nombre { get; set; } = string.Empty; + public string Apellido { get; set; } = string.Empty; + public int IdPerfil { get; set; } + public string NombrePerfil { get; set; } = string.Empty; // Para mostrar en la UI + public bool DebeCambiarClave { get; set; } + public string VerLog { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Usuarios/Perfil.cs b/Backend/GestionIntegral.Api/Models/Usuarios/Perfil.cs new file mode 100644 index 0000000..25b1174 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Usuarios/Perfil.cs @@ -0,0 +1,14 @@ +namespace GestionIntegral.Api.Models.Usuarios +{ + public class Perfil + { + // Columna: id (PK, Identity) + public int Id { get; set; } + + // Columna: perfil (varchar(20), NOT NULL) + public string NombrePerfil { get; set; } = string.Empty; // Renombrado de 'perfil' para evitar conflicto + + // Columna: descPerfil (varchar(150), NULL) + public string? Descripcion { get; set; } // Renombrado de 'descPerfil' + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Usuarios/PerfilHistorico.cs b/Backend/GestionIntegral.Api/Models/Usuarios/PerfilHistorico.cs new file mode 100644 index 0000000..6d94347 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Usuarios/PerfilHistorico.cs @@ -0,0 +1,12 @@ +namespace GestionIntegral.Api.Models.Usuarios +{ + public class PerfilHistorico + { + public int IdPerfil { get; set; } // FK a gral_Perfiles.id + public string NombrePerfil { get; set; } = string.Empty; + public string? Descripcion { get; set; } + public int IdUsuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; // "Insertada", "Modificada", "Eliminada" + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Usuarios/Permiso.cs b/Backend/GestionIntegral.Api/Models/Usuarios/Permiso.cs new file mode 100644 index 0000000..80a75aa --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Usuarios/Permiso.cs @@ -0,0 +1,17 @@ +namespace GestionIntegral.Api.Models.Usuarios +{ + public class Permiso + { + // Columna: id (PK, Identity) + public int Id { get; set; } + + // Columna: modulo (varchar(50), NOT NULL) + public string Modulo { get; set; } = string.Empty; + + // Columna: descPermiso (varchar(150), NOT NULL) + public string DescPermiso { get; set; } = string.Empty; + + // Columna: codAcc (varchar(10), NOT NULL, UNIQUE) + public string CodAcc { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Usuarios/PermisoHistorico.cs b/Backend/GestionIntegral.Api/Models/Usuarios/PermisoHistorico.cs new file mode 100644 index 0000000..f170096 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Usuarios/PermisoHistorico.cs @@ -0,0 +1,14 @@ +namespace GestionIntegral.Api.Models.Usuarios +{ + public class PermisoHistorico + { + public int IdHist { get; set; } // PK de la tabla de historial + public int IdPermiso { get; set; } // FK a gral_Permisos.id + public string Modulo { get; set; } = string.Empty; + public string DescPermiso { get; set; } = string.Empty; + public string CodAcc { get; set; } = string.Empty; + public int IdUsuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Usuario.cs b/Backend/GestionIntegral.Api/Models/Usuarios/Usuario.cs similarity index 94% rename from Backend/GestionIntegral.Api/Models/Usuario.cs rename to Backend/GestionIntegral.Api/Models/Usuarios/Usuario.cs index 1affe9a..04b319a 100644 --- a/Backend/GestionIntegral.Api/Models/Usuario.cs +++ b/Backend/GestionIntegral.Api/Models/Usuarios/Usuario.cs @@ -1,4 +1,4 @@ -namespace GestionIntegral.Api.Models +namespace GestionIntegral.Api.Models.Usuarios { public class Usuario { diff --git a/Backend/GestionIntegral.Api/Models/Usuarios/UsuarioHistorico.cs b/Backend/GestionIntegral.Api/Models/Usuarios/UsuarioHistorico.cs new file mode 100644 index 0000000..4d586a3 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Usuarios/UsuarioHistorico.cs @@ -0,0 +1,33 @@ +namespace GestionIntegral.Api.Models.Usuarios +{ + public class UsuarioHistorico + { + public int IdHist { get; set; } + public int IdUsuario { get; set; } // FK al usuario modificado + + public string? UserAnt { get; set; } + public string UserNvo { get; set; } = string.Empty; + + public bool? HabilitadaAnt { get; set; } + public bool HabilitadaNva { get; set; } + + public bool? SupAdminAnt { get; set; } + public bool SupAdminNvo { get; set; } + + public string? NombreAnt { get; set; } + public string NombreNvo { get; set; } = string.Empty; + + public string? ApellidoAnt { get; set; } + public string ApellidoNvo { get; set; } = string.Empty; + + public int? IdPerfilAnt { get; set; } + public int IdPerfilNvo { get; set; } + + public bool? DebeCambiarClaveAnt { get; set; } + public bool DebeCambiarClaveNva { get; set; } + + public int Id_UsuarioMod { get; set; } // Quién hizo el cambio + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Program.cs b/Backend/GestionIntegral.Api/Program.cs index 3d4ceb8..ddef612 100644 --- a/Backend/GestionIntegral.Api/Program.cs +++ b/Backend/GestionIntegral.Api/Program.cs @@ -2,13 +2,14 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; using GestionIntegral.Api.Data; -using GestionIntegral.Api.Services; using GestionIntegral.Api.Services.Contables; using GestionIntegral.Api.Services.Distribucion; using GestionIntegral.Api.Data.Repositories.Contables; using GestionIntegral.Api.Data.Repositories.Distribucion; using GestionIntegral.Api.Data.Repositories.Impresion; using GestionIntegral.Api.Services.Impresion; +using GestionIntegral.Api.Services.Usuarios; +using GestionIntegral.Api.Data.Repositories.Usuarios; var builder = WebApplication.CreateBuilder(args); @@ -30,6 +31,26 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // --- Configuración de Autenticación JWT --- var jwtSettings = builder.Configuration.GetSection("Jwt"); diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/CanillaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/CanillaService.cs new file mode 100644 index 0000000..9ef99ce --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/CanillaService.cs @@ -0,0 +1,255 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class CanillaService : ICanillaService + { + private readonly ICanillaRepository _canillaRepository; + private readonly IZonaRepository _zonaRepository; + private readonly IEmpresaRepository _empresaRepository; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public CanillaService( + ICanillaRepository canillaRepository, + IZonaRepository zonaRepository, + IEmpresaRepository empresaRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _canillaRepository = canillaRepository; + _zonaRepository = zonaRepository; + _empresaRepository = empresaRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + // CORREGIDO: MapToDto ahora acepta una tupla con tipos anulables + private CanillaDto? MapToDto((Canilla? Canilla, string? NombreZona, string? NombreEmpresa) data) + { + if (data.Canilla == null) return null; + + return new CanillaDto + { + IdCanilla = data.Canilla.IdCanilla, + Legajo = data.Canilla.Legajo, + NomApe = data.Canilla.NomApe, + Parada = data.Canilla.Parada, + IdZona = data.Canilla.IdZona, + NombreZona = data.NombreZona ?? "N/A", // Manejar null + Accionista = data.Canilla.Accionista, + Obs = data.Canilla.Obs, + Empresa = data.Canilla.Empresa, + NombreEmpresa = data.NombreEmpresa ?? "N/A (Accionista)", // Manejar null + Baja = data.Canilla.Baja, + FechaBaja = data.Canilla.FechaBaja?.ToString("dd/MM/yyyy") + }; + } + + public async Task> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos) + { + var canillasData = await _canillaRepository.GetAllAsync(nomApeFilter, legajoFilter, soloActivos); + // Filtrar nulos y asegurar al compilador que no hay nulos en la lista final + return canillasData.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!); + } + + public async Task ObtenerPorIdAsync(int id) + { + var data = await _canillaRepository.GetByIdAsync(id); + // MapToDto ahora devuelve CanillaDto? así que esto es correcto + return MapToDto(data); + } + + public async Task<(CanillaDto? Canilla, string? Error)> CrearAsync(CreateCanillaDto createDto, int idUsuario) + { + if (createDto.Legajo.HasValue && createDto.Legajo != 0 && await _canillaRepository.ExistsByLegajoAsync(createDto.Legajo.Value)) + { + return (null, "El legajo ingresado ya existe para otro canillita."); + } + var zona = await _zonaRepository.GetByIdAsync(createDto.IdZona); // GetByIdAsync de Zona ya considera solo activas + if (zona == null) + { + return (null, "La zona seleccionada no es válida o no está activa."); + } + if (createDto.Empresa != 0) // Solo validar empresa si no es 0 + { + var empresa = await _empresaRepository.GetByIdAsync(createDto.Empresa); + if(empresa == null) + { + return (null, "La empresa seleccionada no es válida."); + } + } + + // CORREGIDO: Usar directamente el valor booleano + if (createDto.Accionista == true && createDto.Empresa != 0) + { + return (null, "Un canillita accionista no debe tener una empresa asignada (Empresa debe ser 0)."); + } + if (createDto.Accionista == false && createDto.Empresa == 0) + { + return (null, "Un canillita no accionista debe tener una empresa asignada (Empresa no puede ser 0)."); + } + + + var nuevoCanilla = new Canilla + { + Legajo = createDto.Legajo == 0 ? null : createDto.Legajo, + NomApe = createDto.NomApe, + Parada = createDto.Parada, + IdZona = createDto.IdZona, + Accionista = createDto.Accionista, + Obs = createDto.Obs, + Empresa = createDto.Empresa, + Baja = false, + FechaBaja = null + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + var canillaCreado = await _canillaRepository.CreateAsync(nuevoCanilla, idUsuario, transaction); + if (canillaCreado == null) throw new DataException("Error al crear el canillita."); + + transaction.Commit(); + + // Para el DTO de respuesta, necesitamos NombreZona y NombreEmpresa + string nombreEmpresaParaDto = "N/A (Accionista)"; + if (canillaCreado.Empresa != 0) + { + var empresaData = await _empresaRepository.GetByIdAsync(canillaCreado.Empresa); + nombreEmpresaParaDto = empresaData?.Nombre ?? "Empresa Desconocida"; + } + + var dtoCreado = new CanillaDto { + IdCanilla = canillaCreado.IdCanilla, Legajo = canillaCreado.Legajo, NomApe = canillaCreado.NomApe, + Parada = canillaCreado.Parada, IdZona = canillaCreado.IdZona, NombreZona = zona.Nombre, // Usar nombre de zona ya obtenido + Accionista = canillaCreado.Accionista, Obs = canillaCreado.Obs, Empresa = canillaCreado.Empresa, + NombreEmpresa = nombreEmpresaParaDto, + Baja = canillaCreado.Baja, FechaBaja = null + }; + + _logger.LogInformation("Canilla ID {IdCanilla} creado por Usuario ID {IdUsuario}.", canillaCreado.IdCanilla, idUsuario); + return (dtoCreado, null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch {} + _logger.LogError(ex, "Error CrearAsync Canilla: {NomApe}", createDto.NomApe); + return (null, $"Error interno al crear el canillita: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCanillaDto updateDto, int idUsuario) + { + var canillaExistente = await _canillaRepository.GetByIdSimpleAsync(id); + if (canillaExistente == null) return (false, "Canillita no encontrado."); + + if (updateDto.Legajo.HasValue && updateDto.Legajo != 0 && await _canillaRepository.ExistsByLegajoAsync(updateDto.Legajo.Value, id)) + { + return (false, "El legajo ingresado ya existe para otro canillita."); + } + if (await _zonaRepository.GetByIdAsync(updateDto.IdZona) == null) // GetByIdAsync de Zona ya considera solo activas + { + return (false, "La zona seleccionada no es válida o no está activa."); + } + if (updateDto.Empresa != 0) // Solo validar empresa si no es 0 + { + var empresa = await _empresaRepository.GetByIdAsync(updateDto.Empresa); + if(empresa == null) + { + return (false, "La empresa seleccionada no es válida."); + } + } + + // Usar directamente el valor booleano para Accionista + if (updateDto.Accionista == true && updateDto.Empresa != 0) + { + // Al ser 'bool', no puede ser null. La comparación explícita con 'true'/'false' es para claridad. + return (false, "Un canillita accionista no debe tener una empresa asignada (Empresa debe ser 0)."); + } + if (updateDto.Accionista == false && updateDto.Empresa == 0) + { + return (false, "Un canillita no accionista debe tener una empresa asignada (Empresa no puede ser 0)."); + } + + // Mapear DTO a entidad existente + canillaExistente.Legajo = updateDto.Legajo == 0 ? null : updateDto.Legajo; + canillaExistente.NomApe = updateDto.NomApe; + canillaExistente.Parada = updateDto.Parada; + canillaExistente.IdZona = updateDto.IdZona; + canillaExistente.Accionista = updateDto.Accionista; // Aquí Accionista ya es bool + canillaExistente.Obs = updateDto.Obs; + canillaExistente.Empresa = updateDto.Empresa; + // Baja y FechaBaja se manejan por ToggleBajaAsync + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + var actualizado = await _canillaRepository.UpdateAsync(canillaExistente, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar el canillita."); + transaction.Commit(); + _logger.LogInformation("Canilla ID {IdCanilla} actualizado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { + try { transaction.Rollback(); } catch {} + return (false, "Canillita no encontrado durante la actualización."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch {} + _logger.LogError(ex, "Error ActualizarAsync Canilla ID: {IdCanilla}", id); + return (false, $"Error interno al actualizar el canillita: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ToggleBajaAsync(int id, bool darDeBaja, int idUsuario) + { + var canilla = await _canillaRepository.GetByIdSimpleAsync(id); + if (canilla == null) return (false, "Canillita no encontrado."); + + if (canilla.Baja == darDeBaja) + { + return (false, darDeBaja ? "El canillita ya está dado de baja." : "El canillita ya está activo."); + } + + DateTime? fechaBaja = darDeBaja ? DateTime.Now : (DateTime?)null; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var success = await _canillaRepository.ToggleBajaAsync(id, darDeBaja, fechaBaja, idUsuario, transaction); + if (!success) throw new DataException("Error al cambiar estado de baja del canillita."); + transaction.Commit(); + _logger.LogInformation("Estado de baja cambiado a {EstadoBaja} para Canilla ID {IdCanilla} por Usuario ID {IdUsuario}.", darDeBaja, id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { + try { transaction.Rollback(); } catch {} + return (false, "Canillita no encontrado durante el cambio de estado de baja."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch {} + _logger.LogError(ex, "Error ToggleBajaAsync Canilla ID: {IdCanilla}", id); + return (false, $"Error interno al cambiar estado de baja: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/DistribuidorService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/DistribuidorService.cs new file mode 100644 index 0000000..a1f1ca6 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/DistribuidorService.cs @@ -0,0 +1,197 @@ +// src/Services/Distribucion/DistribuidorService.cs +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Contables; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class DistribuidorService : IDistribuidorService + { + private readonly IDistribuidorRepository _distribuidorRepository; + private readonly ISaldoRepository _saldoRepository; + private readonly IEmpresaRepository _empresaRepository; + private readonly IZonaRepository _zonaRepository; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public DistribuidorService( + IDistribuidorRepository distribuidorRepository, + ISaldoRepository saldoRepository, + IEmpresaRepository empresaRepository, + IZonaRepository zonaRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _distribuidorRepository = distribuidorRepository; + _saldoRepository = saldoRepository; + _empresaRepository = empresaRepository; + _zonaRepository = zonaRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private DistribuidorDto? MapToDto((Distribuidor? Distribuidor, string? NombreZona) data) + { + if (data.Distribuidor == null) return null; + + return new DistribuidorDto + { + IdDistribuidor = data.Distribuidor.IdDistribuidor, + Nombre = data.Distribuidor.Nombre, + Contacto = data.Distribuidor.Contacto, + NroDoc = data.Distribuidor.NroDoc, + IdZona = data.Distribuidor.IdZona, + NombreZona = data.NombreZona, // Ya es anulable en el DTO y en la tupla + Calle = data.Distribuidor.Calle, + Numero = data.Distribuidor.Numero, + Piso = data.Distribuidor.Piso, + Depto = data.Distribuidor.Depto, + Telefono = data.Distribuidor.Telefono, + Email = data.Distribuidor.Email, + Localidad = data.Distribuidor.Localidad + }; + } + + public async Task> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter) + { + var data = await _distribuidorRepository.GetAllAsync(nombreFilter, nroDocFilter); + // Filtrar nulos y asegurar al compilador que no hay nulos en la lista final + return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!); + } + + public async Task ObtenerPorIdAsync(int id) + { + var data = await _distribuidorRepository.GetByIdAsync(id); + // MapToDto ahora devuelve DistribuidorDto? + return MapToDto(data); + } + + public async Task<(DistribuidorDto? Distribuidor, string? Error)> CrearAsync(CreateDistribuidorDto createDto, int idUsuario) + { + if (await _distribuidorRepository.ExistsByNroDocAsync(createDto.NroDoc)) + return (null, "El número de documento ya existe para otro distribuidor."); + if (await _distribuidorRepository.ExistsByNameAsync(createDto.Nombre)) + return (null, "El nombre del distribuidor ya existe."); + if (createDto.IdZona.HasValue && await _zonaRepository.GetByIdAsync(createDto.IdZona.Value) == null) + return (null, "La zona seleccionada no es válida o no está activa."); + + var nuevoDistribuidor = new Distribuidor + { + Nombre = createDto.Nombre, Contacto = createDto.Contacto, NroDoc = createDto.NroDoc, IdZona = createDto.IdZona, + Calle = createDto.Calle, Numero = createDto.Numero, Piso = createDto.Piso, Depto = createDto.Depto, + Telefono = createDto.Telefono, Email = createDto.Email, Localidad = createDto.Localidad + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + var distribuidorCreado = await _distribuidorRepository.CreateAsync(nuevoDistribuidor, idUsuario, transaction); + if (distribuidorCreado == null) throw new DataException("Error al crear el distribuidor."); + + var empresas = await _empresaRepository.GetAllAsync(null, null); + foreach (var empresa in empresas) + { + bool saldoCreado = await _saldoRepository.CreateSaldoInicialAsync("Distribuidores", distribuidorCreado.IdDistribuidor, empresa.IdEmpresa, transaction); + if (!saldoCreado) + { + throw new DataException($"Falló al crear saldo inicial para el distribuidor {distribuidorCreado.IdDistribuidor} en la empresa {empresa.IdEmpresa}."); + } + } + + transaction.Commit(); + var dataCompleta = await _distribuidorRepository.GetByIdAsync(distribuidorCreado.IdDistribuidor); + _logger.LogInformation("Distribuidor ID {IdDistribuidor} creado por Usuario ID {IdUsuario}.", distribuidorCreado.IdDistribuidor, idUsuario); + // MapToDto ahora devuelve DistribuidorDto? + return (MapToDto(dataCompleta), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch {} + _logger.LogError(ex, "Error CrearAsync Distribuidor: {Nombre}", createDto.Nombre); + return (null, $"Error interno al crear el distribuidor: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateDistribuidorDto updateDto, int idUsuario) + { + var distribuidorExistente = await _distribuidorRepository.GetByIdSimpleAsync(id); + if (distribuidorExistente == null) return (false, "Distribuidor no encontrado."); + + if (await _distribuidorRepository.ExistsByNroDocAsync(updateDto.NroDoc, id)) + return (false, "El número de documento ya existe para otro distribuidor."); + // CORREGIDO: La tupla de retorno para la validación de nombre debe ser (bool, string?) + if (await _distribuidorRepository.ExistsByNameAsync(updateDto.Nombre, id)) + return (false, "El nombre del distribuidor ya existe."); // Antes (null, ...) + if (updateDto.IdZona.HasValue && await _zonaRepository.GetByIdAsync(updateDto.IdZona.Value) == null) + return (false, "La zona seleccionada no es válida o no está activa."); + + distribuidorExistente.Nombre = updateDto.Nombre; distribuidorExistente.Contacto = updateDto.Contacto; + distribuidorExistente.NroDoc = updateDto.NroDoc; distribuidorExistente.IdZona = updateDto.IdZona; + distribuidorExistente.Calle = updateDto.Calle; distribuidorExistente.Numero = updateDto.Numero; + distribuidorExistente.Piso = updateDto.Piso; distribuidorExistente.Depto = updateDto.Depto; + distribuidorExistente.Telefono = updateDto.Telefono; distribuidorExistente.Email = updateDto.Email; + distribuidorExistente.Localidad = updateDto.Localidad; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var actualizado = await _distribuidorRepository.UpdateAsync(distribuidorExistente, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar."); + transaction.Commit(); + _logger.LogInformation("Distribuidor ID {IdDistribuidor} actualizado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch {} return (false, "Distribuidor no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch {} + _logger.LogError(ex, "Error ActualizarAsync Distribuidor ID: {IdDistribuidor}", id); + return (false, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) + { + var distribuidorExistente = await _distribuidorRepository.GetByIdSimpleAsync(id); + if (distribuidorExistente == null) return (false, "Distribuidor no encontrado."); + + if (await _distribuidorRepository.IsInUseAsync(id)) + return (false, "No se puede eliminar. El distribuidor tiene movimientos o configuraciones asociadas."); + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + // Lógica para eliminar saldos asociados (si se implementa) + // var saldosEliminados = await _saldoRepository.DeleteSaldosByDestinoAsync("Distribuidores", id, transaction); + // if (!saldosEliminados) throw new DataException("Error al eliminar saldos del distribuidor."); + + var eliminado = await _distribuidorRepository.DeleteAsync(id, idUsuario, transaction); + if (!eliminado) throw new DataException("Error al eliminar."); + transaction.Commit(); + _logger.LogInformation("Distribuidor ID {IdDistribuidor} eliminado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch {} return (false, "Distribuidor no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch {} + _logger.LogError(ex, "Error EliminarAsync Distribuidor ID: {IdDistribuidor}", id); + return (false, $"Error interno: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/ICanillaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/ICanillaService.cs new file mode 100644 index 0000000..9d69df5 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/ICanillaService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface ICanillaService + { + Task> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos); + Task ObtenerPorIdAsync(int id); + Task<(CanillaDto? Canilla, string? Error)> CrearAsync(CreateCanillaDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCanillaDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> ToggleBajaAsync(int id, bool darDeBaja, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IDistribuidorService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IDistribuidorService.cs new file mode 100644 index 0000000..0512897 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IDistribuidorService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IDistribuidorService + { + Task> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter); + Task ObtenerPorIdAsync(int id); + Task<(DistribuidorDto? Distribuidor, string? Error)> CrearAsync(CreateDistribuidorDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateDistribuidorDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IOtroDestinoService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IOtroDestinoService.cs new file mode 100644 index 0000000..dc10066 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IOtroDestinoService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IOtroDestinoService + { + Task> ObtenerTodosAsync(string? nombreFilter); + Task ObtenerPorIdAsync(int id); + Task<(OtroDestinoDto? Destino, string? Error)> CrearAsync(CreateOtroDestinoDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateOtroDestinoDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IPrecioService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IPrecioService.cs new file mode 100644 index 0000000..e466c1b --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IPrecioService.cs @@ -0,0 +1,16 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IPrecioService + { + Task> ObtenerPorPublicacionIdAsync(int idPublicacion); + Task ObtenerPorIdAsync(int idPrecio); // Para editar un precio específico + Task<(PrecioDto? Precio, string? Error)> CrearAsync(CreatePrecioDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int idPrecio, UpdatePrecioDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int idPrecio, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IPublicacionService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IPublicacionService.cs new file mode 100644 index 0000000..9e4defb --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IPublicacionService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IPublicacionService + { + Task> ObtenerTodasAsync(string? nombreFilter, int? idEmpresaFilter, bool? soloHabilitadas); + Task ObtenerPorIdAsync(int id); + Task<(PublicacionDto? Publicacion, string? Error)> CrearAsync(CreatePublicacionDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdatePublicacionDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/OtroDestinoService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/OtroDestinoService.cs new file mode 100644 index 0000000..9097e6c --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/OtroDestinoService.cs @@ -0,0 +1,143 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class OtroDestinoService : IOtroDestinoService + { + private readonly IOtroDestinoRepository _otroDestinoRepository; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public OtroDestinoService(IOtroDestinoRepository otroDestinoRepository, DbConnectionFactory connectionFactory, ILogger logger) + { + _otroDestinoRepository = otroDestinoRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private OtroDestinoDto MapToDto(OtroDestino destino) => new OtroDestinoDto + { + IdDestino = destino.IdDestino, + Nombre = destino.Nombre, + Obs = destino.Obs + }; + + public async Task> ObtenerTodosAsync(string? nombreFilter) + { + var destinos = await _otroDestinoRepository.GetAllAsync(nombreFilter); + return destinos.Select(MapToDto); + } + + public async Task ObtenerPorIdAsync(int id) + { + var destino = await _otroDestinoRepository.GetByIdAsync(id); + return destino == null ? null : MapToDto(destino); + } + + public async Task<(OtroDestinoDto? Destino, string? Error)> CrearAsync(CreateOtroDestinoDto createDto, int idUsuario) + { + if (await _otroDestinoRepository.ExistsByNameAsync(createDto.Nombre)) + { + return (null, "El nombre del destino ya existe."); + } + + var nuevoDestino = new OtroDestino { Nombre = createDto.Nombre, Obs = createDto.Obs }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var destinoCreado = await _otroDestinoRepository.CreateAsync(nuevoDestino, idUsuario, transaction); + if (destinoCreado == null) throw new DataException("La creación en el repositorio devolvió null."); + + transaction.Commit(); + _logger.LogInformation("OtroDestino ID {IdDestino} creado por Usuario ID {IdUsuario}.", destinoCreado.IdDestino, idUsuario); + return (MapToDto(destinoCreado), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { /* Log Rollback Error */ } + _logger.LogError(ex, "Error CrearAsync OtroDestino. Nombre: {Nombre}", createDto.Nombre); + return (null, $"Error interno al crear el destino: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateOtroDestinoDto updateDto, int idUsuario) + { + if (await _otroDestinoRepository.ExistsByNameAsync(updateDto.Nombre, id)) + { + return (false, "El nombre del destino ya existe para otro registro."); + } + + var destinoAActualizar = new OtroDestino { IdDestino = id, Nombre = updateDto.Nombre, Obs = updateDto.Obs }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var actualizado = await _otroDestinoRepository.UpdateAsync(destinoAActualizar, idUsuario, transaction); + if (!actualizado) throw new DataException("La operación de actualización no afectó ninguna fila."); + + transaction.Commit(); + _logger.LogInformation("OtroDestino ID {IdDestino} actualizado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) + { + try { transaction.Rollback(); } catch { /* Log Rollback Error */ } + return (false, "Otro Destino no encontrado."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { /* Log Rollback Error */ } + _logger.LogError(ex, "Error ActualizarAsync OtroDestino ID: {Id}", id); + return (false, $"Error interno al actualizar el destino: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) + { + if (await _otroDestinoRepository.IsInUseAsync(id)) + { + return (false, "No se puede eliminar. El destino está siendo utilizado en salidas registradas."); + } + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var eliminado = await _otroDestinoRepository.DeleteAsync(id, idUsuario, transaction); + if (!eliminado) throw new DataException("La operación de eliminación no afectó ninguna fila."); + + transaction.Commit(); + _logger.LogInformation("OtroDestino ID {IdDestino} eliminado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) + { + try { transaction.Rollback(); } catch { /* Log Rollback Error */ } + return (false, "Otro Destino no encontrado."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { /* Log Rollback Error */ } + _logger.LogError(ex, "Error EliminarAsync OtroDestino ID: {Id}", id); + return (false, $"Error interno al eliminar el destino: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/PrecioService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/PrecioService.cs new file mode 100644 index 0000000..d3a6608 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/PrecioService.cs @@ -0,0 +1,230 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class PrecioService : IPrecioService + { + private readonly IPrecioRepository _precioRepository; + private readonly IPublicacionRepository _publicacionRepository; // Para validar IdPublicacion + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public PrecioService( + IPrecioRepository precioRepository, + IPublicacionRepository publicacionRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _precioRepository = precioRepository; + _publicacionRepository = publicacionRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private PrecioDto MapToDto(Precio precio) => new PrecioDto + { + IdPrecio = precio.IdPrecio, + IdPublicacion = precio.IdPublicacion, + VigenciaD = precio.VigenciaD.ToString("yyyy-MM-dd"), + VigenciaH = precio.VigenciaH?.ToString("yyyy-MM-dd"), + Lunes = precio.Lunes, Martes = precio.Martes, Miercoles = precio.Miercoles, + Jueves = precio.Jueves, Viernes = precio.Viernes, Sabado = precio.Sabado, Domingo = precio.Domingo + }; + + public async Task> ObtenerPorPublicacionIdAsync(int idPublicacion) + { + var precios = await _precioRepository.GetByPublicacionIdAsync(idPublicacion); + return precios.Select(MapToDto); + } + public async Task ObtenerPorIdAsync(int idPrecio) + { + var precio = await _precioRepository.GetByIdAsync(idPrecio); + return precio == null ? null : MapToDto(precio); + } + + + public async Task<(PrecioDto? Precio, string? Error)> CrearAsync(CreatePrecioDto createDto, int idUsuario) + { + if (await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion) == null) + return (null, "La publicación especificada no existe."); + + // Validar que no haya solapamiento de fechas o que VigenciaD sea lógica + // Un precio no puede empezar antes que el VigenciaH de un precio anterior no cerrado. + // Y no puede empezar en una fecha donde ya existe un precio activo para esa publicación. + var precioActivoEnFecha = await _precioRepository.GetActiveByPublicacionAndDateAsync(createDto.IdPublicacion, createDto.VigenciaD); + if (precioActivoEnFecha != null) + { + return (null, $"Ya existe un período de precios activo para esta publicación en la fecha {createDto.VigenciaD:dd/MM/yyyy}. Primero debe cerrar el período anterior."); + } + + + var nuevoPrecio = new Precio + { + IdPublicacion = createDto.IdPublicacion, + VigenciaD = createDto.VigenciaD.Date, // Asegurar que solo sea fecha + VigenciaH = null, // Se establece al crear el siguiente o al cerrar manualmente + Lunes = createDto.Lunes ?? 0, Martes = createDto.Martes ?? 0, Miercoles = createDto.Miercoles ?? 0, + Jueves = createDto.Jueves ?? 0, Viernes = createDto.Viernes ?? 0, Sabado = createDto.Sabado ?? 0, Domingo = createDto.Domingo ?? 0 + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + // 1. Buscar el precio anterior activo para esta publicación que no tenga VigenciaH + var precioAnterior = await _precioRepository.GetPreviousActivePriceAsync(createDto.IdPublicacion, nuevoPrecio.VigenciaD, transaction); + + if (precioAnterior != null) + { + if (precioAnterior.VigenciaD >= nuevoPrecio.VigenciaD) + { + transaction.Rollback(); + return (null, $"La fecha de inicio del nuevo precio ({nuevoPrecio.VigenciaD:dd/MM/yyyy}) no puede ser anterior o igual a la del último precio vigente ({precioAnterior.VigenciaD:dd/MM/yyyy})."); + } + // 2. Si existe, actualizar su VigenciaH al día anterior de la VigenciaD del nuevo precio + precioAnterior.VigenciaH = nuevoPrecio.VigenciaD.AddDays(-1); + bool actualizado = await _precioRepository.UpdateAsync(precioAnterior, idUsuario, transaction); // Usar un idUsuario de sistema/auditoría para esta acción automática si es necesario + if (!actualizado) + { + throw new DataException("No se pudo cerrar el período de precio anterior."); + } + _logger.LogInformation("Precio anterior ID {IdPrecioAnterior} cerrado con VigenciaH {VigenciaH}.", precioAnterior.IdPrecio, precioAnterior.VigenciaH); + } + + // 3. Crear el nuevo registro de precio + var precioCreado = await _precioRepository.CreateAsync(nuevoPrecio, idUsuario, transaction); + if (precioCreado == null) throw new DataException("Error al crear el nuevo precio."); + + transaction.Commit(); + _logger.LogInformation("Precio ID {IdPrecio} creado para Publicación ID {IdPublicacion} por Usuario ID {IdUsuario}.", precioCreado.IdPrecio, precioCreado.IdPublicacion, idUsuario); + return (MapToDto(precioCreado), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error CrearAsync Precio para Publicación ID {IdPublicacion}", createDto.IdPublicacion); + return (null, $"Error interno al crear el precio: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int idPrecio, UpdatePrecioDto updateDto, int idUsuario) + { + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + var precioExistente = await _precioRepository.GetByIdAsync(idPrecio); // Obtener dentro de la TX por si acaso + if (precioExistente == null) return (false, "Período de precio no encontrado."); + + // En una actualización, principalmente actualizamos los montos de los días. + // VigenciaH se puede actualizar para cerrar un período explícitamente. + // No se permite cambiar IdPublicacion ni VigenciaD aquí. + // Si VigenciaH se establece, debe ser >= VigenciaD + if (updateDto.VigenciaH.HasValue && updateDto.VigenciaH.Value.Date < precioExistente.VigenciaD.Date) + { + return (false, "La Vigencia Hasta no puede ser anterior a la Vigencia Desde."); + } + // Adicional: si se establece VigenciaH, verificar que no haya precios posteriores que se solapen + if (updateDto.VigenciaH.HasValue) + { + var preciosPosteriores = await _precioRepository.GetByPublicacionIdAsync(precioExistente.IdPublicacion); + if (preciosPosteriores.Any(p => p.IdPrecio != idPrecio && p.VigenciaD.Date <= updateDto.VigenciaH.Value.Date && p.VigenciaD.Date > precioExistente.VigenciaD.Date )) + { + return (false, "No se puede cerrar este período porque existen períodos de precios posteriores que se solaparían. Elimine o ajuste los períodos posteriores primero."); + } + } + + + precioExistente.Lunes = updateDto.Lunes ?? precioExistente.Lunes; + precioExistente.Martes = updateDto.Martes ?? precioExistente.Martes; + precioExistente.Miercoles = updateDto.Miercoles ?? precioExistente.Miercoles; + precioExistente.Jueves = updateDto.Jueves ?? precioExistente.Jueves; + precioExistente.Viernes = updateDto.Viernes ?? precioExistente.Viernes; + precioExistente.Sabado = updateDto.Sabado ?? precioExistente.Sabado; + precioExistente.Domingo = updateDto.Domingo ?? precioExistente.Domingo; + if (updateDto.VigenciaH.HasValue) // Solo actualizar VigenciaH si se proporciona + { + precioExistente.VigenciaH = updateDto.VigenciaH.Value.Date; + } + + + var actualizado = await _precioRepository.UpdateAsync(precioExistente, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar el período de precio."); + + transaction.Commit(); + _logger.LogInformation("Precio ID {IdPrecio} actualizado por Usuario ID {IdUsuario}.", idPrecio, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch {} return (false, "Período de precio no encontrado.");} + catch (Exception ex) + { + try { transaction.Rollback(); } catch {} + _logger.LogError(ex, "Error ActualizarAsync Precio ID: {IdPrecio}", idPrecio); + return (false, $"Error interno al actualizar el período de precio: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int idPrecio, int idUsuario) + { + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var precioAEliminar = await _precioRepository.GetByIdAsync(idPrecio); + if (precioAEliminar == null) return (false, "Período de precio no encontrado."); + + // Lógica de ajuste de VigenciaH del período anterior si este que se elimina era el "último" abierto. + // Si el precio a eliminar tiene un VigenciaH == null (estaba activo indefinidamente) + // Y existe un precio anterior para la misma publicación. + // Entonces, el VigenciaH de ese precio anterior debe volver a ser NULL. + if (precioAEliminar.VigenciaH == null) + { + var todosLosPreciosPub = (await _precioRepository.GetByPublicacionIdAsync(precioAEliminar.IdPublicacion)) + .OrderByDescending(p => p.VigenciaD).ToList(); + + var indiceActual = todosLosPreciosPub.FindIndex(p=> p.IdPrecio == idPrecio); + if(indiceActual != -1 && (indiceActual + 1) < todosLosPreciosPub.Count) + { + var precioAnteriorDirecto = todosLosPreciosPub[indiceActual + 1]; + // Solo si el precioAnteriorDirecto fue cerrado por este que se elimina + if(precioAnteriorDirecto.VigenciaH.HasValue && precioAnteriorDirecto.VigenciaH.Value.Date == precioAEliminar.VigenciaD.AddDays(-1).Date) + { + precioAnteriorDirecto.VigenciaH = null; + await _precioRepository.UpdateAsync(precioAnteriorDirecto, idUsuario, transaction); // Usar un ID de auditoría adecuado + _logger.LogInformation("Precio anterior ID {IdPrecioAnterior} reabierto (VigenciaH a NULL) tras eliminación de Precio ID {IdPrecioEliminado}.", precioAnteriorDirecto.IdPrecio, idPrecio); + } + } + } + + + var eliminado = await _precioRepository.DeleteAsync(idPrecio, idUsuario, transaction); + if (!eliminado) throw new DataException("Error al eliminar el período de precio."); + + transaction.Commit(); + _logger.LogInformation("Precio ID {IdPrecio} eliminado por Usuario ID {IdUsuario}.", idPrecio, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch {} return (false, "Período de precio no encontrado."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch {} + _logger.LogError(ex, "Error EliminarAsync Precio ID: {IdPrecio}", idPrecio); + return (false, $"Error interno al eliminar el período de precio: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/PublicacionService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/PublicacionService.cs new file mode 100644 index 0000000..33f40ce --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/PublicacionService.cs @@ -0,0 +1,206 @@ +// src/Services/Distribucion/PublicacionService.cs +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class PublicacionService : IPublicacionService + { + private readonly IPublicacionRepository _publicacionRepository; + private readonly IEmpresaRepository _empresaRepository; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + private readonly IPrecioRepository _precioRepository; + private readonly IRecargoZonaRepository _recargoZonaRepository; + private readonly IPorcPagoRepository _porcPagoRepository; + private readonly IPorcMonCanillaRepository _porcMonCanillaRepository; + private readonly IPubliSeccionRepository _publiSeccionRepository; + + public PublicacionService( + IPublicacionRepository publicacionRepository, + IEmpresaRepository empresaRepository, + DbConnectionFactory connectionFactory, + ILogger logger, + IPrecioRepository precioRepository, + IRecargoZonaRepository recargoZonaRepository, + IPorcPagoRepository porcPagoRepository, + IPorcMonCanillaRepository porcMonCanillaRepository, + IPubliSeccionRepository publiSeccionRepository) + { + _publicacionRepository = publicacionRepository; + _empresaRepository = empresaRepository; + _connectionFactory = connectionFactory; + _logger = logger; + _precioRepository = precioRepository; + _recargoZonaRepository = recargoZonaRepository; + _porcPagoRepository = porcPagoRepository; + _porcMonCanillaRepository = porcMonCanillaRepository; + _publiSeccionRepository = publiSeccionRepository; + } + + private PublicacionDto? MapToDto((Publicacion? Publicacion, string? NombreEmpresa) data) + { + if (data.Publicacion == null) return null; // Si la publicación es null, no se puede mapear + + return new PublicacionDto + { + IdPublicacion = data.Publicacion.IdPublicacion, + Nombre = data.Publicacion.Nombre, + Observacion = data.Publicacion.Observacion, + IdEmpresa = data.Publicacion.IdEmpresa, + NombreEmpresa = data.NombreEmpresa ?? "Empresa Desconocida", // Manejar null para NombreEmpresa + CtrlDevoluciones = data.Publicacion.CtrlDevoluciones, + Habilitada = data.Publicacion.Habilitada ?? true // Asumir true si es null desde BD + }; + } + + public async Task> ObtenerTodasAsync(string? nombreFilter, int? idEmpresaFilter, bool? soloHabilitadas) + { + var data = await _publicacionRepository.GetAllAsync(nombreFilter, idEmpresaFilter, soloHabilitadas); + // Filtrar los nulos que MapToDto podría devolver (si alguna tupla tuviera Publicacion null) + return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!); + } + + public async Task ObtenerPorIdAsync(int id) + { + var data = await _publicacionRepository.GetByIdAsync(id); + // MapToDto ahora devuelve PublicacionDto? + return MapToDto(data); + } + + public async Task<(PublicacionDto? Publicacion, string? Error)> CrearAsync(CreatePublicacionDto createDto, int idUsuario) + { + if (await _empresaRepository.GetByIdAsync(createDto.IdEmpresa) == null) + return (null, "La empresa seleccionada no es válida."); + if (await _publicacionRepository.ExistsByNameAndEmpresaAsync(createDto.Nombre, createDto.IdEmpresa)) + return (null, "Ya existe una publicación con ese nombre para la empresa seleccionada."); + + var nuevaPublicacion = new Publicacion + { + Nombre = createDto.Nombre, + Observacion = createDto.Observacion, + IdEmpresa = createDto.IdEmpresa, + CtrlDevoluciones = createDto.CtrlDevoluciones, + Habilitada = createDto.Habilitada + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var creada = await _publicacionRepository.CreateAsync(nuevaPublicacion, idUsuario, transaction); + if (creada == null) throw new DataException("Error al crear publicación."); + + transaction.Commit(); + var dataCompleta = await _publicacionRepository.GetByIdAsync(creada.IdPublicacion); // Para obtener NombreEmpresa + _logger.LogInformation("Publicación ID {Id} creada por Usuario ID {UserId}.", creada.IdPublicacion, idUsuario); + // MapToDto ahora devuelve PublicacionDto? + return (MapToDto(dataCompleta), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error CrearAsync Publicacion: {Nombre}", createDto.Nombre); + return (null, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdatePublicacionDto updateDto, int idUsuario) + { + var existente = await _publicacionRepository.GetByIdSimpleAsync(id); + if (existente == null) return (false, "Publicación no encontrada."); + if (await _empresaRepository.GetByIdAsync(updateDto.IdEmpresa) == null) + return (false, "La empresa seleccionada no es válida."); + if (await _publicacionRepository.ExistsByNameAndEmpresaAsync(updateDto.Nombre, updateDto.IdEmpresa, id)) + return (false, "Ya existe otra publicación con ese nombre para la empresa seleccionada."); + + existente.Nombre = updateDto.Nombre; existente.Observacion = updateDto.Observacion; + existente.IdEmpresa = updateDto.IdEmpresa; existente.CtrlDevoluciones = updateDto.CtrlDevoluciones; + existente.Habilitada = updateDto.Habilitada; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var actualizado = await _publicacionRepository.UpdateAsync(existente, idUsuario, transaction); + if (!actualizado) throw new DataException("Error al actualizar."); + transaction.Commit(); + _logger.LogInformation("Publicación ID {Id} actualizada por Usuario ID {UserId}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Publicación no encontrada."); } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { } + _logger.LogError(ex, "Error ActualizarAsync Publicacion ID: {Id}", id); + return (false, $"Error interno: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) // idUsuario es el que realiza la acción + { + var existente = await _publicacionRepository.GetByIdSimpleAsync(id); + if (existente == null) return (false, "Publicación no encontrada."); + + if (await _publicacionRepository.IsInUseAsync(id)) + { + return (false, "No se puede eliminar. La publicación tiene datos transaccionales o de configuración relacionados."); + } + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + _logger.LogInformation("Iniciando eliminación de dependencias para Publicación ID: {IdPublicacion} por Usuario ID: {idUsuario}", id, idUsuario); + + // Pasar idUsuario a los métodos de borrado para auditoría + await _precioRepository.DeleteByPublicacionIdAsync(id, idUsuario, transaction); + _logger.LogDebug("Precios eliminados para Publicación ID: {IdPublicacion}", id); + + await _recargoZonaRepository.DeleteByPublicacionIdAsync(id, idUsuario, transaction); + _logger.LogDebug("RecargosZona eliminados para Publicación ID: {IdPublicacion}", id); + + await _porcPagoRepository.DeleteByPublicacionIdAsync(id, idUsuario, transaction); + _logger.LogDebug("PorcPago eliminados para Publicación ID: {IdPublicacion}", id); + + await _porcMonCanillaRepository.DeleteByPublicacionIdAsync(id, idUsuario, transaction); + _logger.LogDebug("PorcMonCanilla eliminados para Publicación ID: {IdPublicacion}", id); + + await _publiSeccionRepository.DeleteByPublicacionIdAsync(id, idUsuario, transaction); + _logger.LogDebug("PubliSecciones eliminadas para Publicación ID: {IdPublicacion}", id); + + var eliminado = await _publicacionRepository.DeleteAsync(id, idUsuario, transaction); + if (!eliminado) + { + throw new DataException("Error al eliminar la publicación principal después de sus dependencias."); + } + + transaction.Commit(); + _logger.LogInformation("Publicación ID {Id} y sus dependencias eliminadas por Usuario ID {UserId}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException knfEx) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback tras KeyNotFoundException."); } + _logger.LogWarning(knfEx, "Entidad no encontrada durante la eliminación de Publicación ID {Id}.", id); + return (false, "Una entidad relacionada o la publicación misma no fue encontrada durante la eliminación."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback tras excepción general."); } + _logger.LogError(ex, "Error EliminarAsync Publicacion ID: {Id}", id); + return (false, $"Error interno al eliminar la publicación y sus dependencias: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/AuthService.cs b/Backend/GestionIntegral.Api/Services/Usuarios/AuthService.cs similarity index 98% rename from Backend/GestionIntegral.Api/Services/AuthService.cs rename to Backend/GestionIntegral.Api/Services/Usuarios/AuthService.cs index dd65b99..d093170 100644 --- a/Backend/GestionIntegral.Api/Services/AuthService.cs +++ b/Backend/GestionIntegral.Api/Services/Usuarios/AuthService.cs @@ -1,13 +1,13 @@ -using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Usuarios; using GestionIntegral.Api.Dtos; -using GestionIntegral.Api.Models; +using GestionIntegral.Api.Models.Usuarios; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; -namespace GestionIntegral.Api.Services +namespace GestionIntegral.Api.Services.Usuarios { public class AuthService : IAuthService { diff --git a/Backend/GestionIntegral.Api/Services/IAuthService.cs b/Backend/GestionIntegral.Api/Services/Usuarios/IAuthService.cs similarity index 84% rename from Backend/GestionIntegral.Api/Services/IAuthService.cs rename to Backend/GestionIntegral.Api/Services/Usuarios/IAuthService.cs index 5965dc3..821b628 100644 --- a/Backend/GestionIntegral.Api/Services/IAuthService.cs +++ b/Backend/GestionIntegral.Api/Services/Usuarios/IAuthService.cs @@ -1,6 +1,6 @@ using GestionIntegral.Api.Dtos; -namespace GestionIntegral.Api.Services +namespace GestionIntegral.Api.Services.Usuarios { public interface IAuthService { diff --git a/Backend/GestionIntegral.Api/Services/Usuarios/IPerfilService.cs b/Backend/GestionIntegral.Api/Services/Usuarios/IPerfilService.cs new file mode 100644 index 0000000..d526246 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Usuarios/IPerfilService.cs @@ -0,0 +1,17 @@ +using GestionIntegral.Api.Dtos.Usuarios; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Usuarios +{ + public interface IPerfilService + { + Task> ObtenerTodosAsync(string? nombreFilter); + Task ObtenerPorIdAsync(int id); + Task<(PerfilDto? Perfil, string? Error)> CrearAsync(CreatePerfilDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdatePerfilDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); + Task> ObtenerPermisosAsignadosAsync(int idPerfil); + Task<(bool Exito, string? Error)> ActualizarPermisosAsignadosAsync(int idPerfil, ActualizarPermisosPerfilRequestDto request, int idUsuarioModificador); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Usuarios/IPermisoService.cs b/Backend/GestionIntegral.Api/Services/Usuarios/IPermisoService.cs new file mode 100644 index 0000000..0a82a2c --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Usuarios/IPermisoService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Usuarios; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Usuarios +{ + public interface IPermisoService + { + Task> ObtenerTodosAsync(string? moduloFilter, string? codAccFilter); + Task ObtenerPorIdAsync(int id); + Task<(PermisoDto? Permiso, string? Error)> CrearAsync(CreatePermisoDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdatePermisoDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Usuarios/IUsuarioService.cs b/Backend/GestionIntegral.Api/Services/Usuarios/IUsuarioService.cs new file mode 100644 index 0000000..da121ee --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Usuarios/IUsuarioService.cs @@ -0,0 +1,18 @@ +using GestionIntegral.Api.Dtos.Usuarios; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Usuarios +{ + public interface IUsuarioService + { + Task> ObtenerTodosAsync(string? userFilter, string? nombreFilter); + Task ObtenerPorIdAsync(int id); + Task<(UsuarioDto? Usuario, string? Error)> CrearAsync(CreateUsuarioRequestDto createDto, int idUsuarioCreador); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateUsuarioRequestDto updateDto, int idUsuarioModificador); + Task<(bool Exito, string? Error)> SetPasswordAsync(int userId, SetPasswordRequestDto setPasswordDto, int idUsuarioModificador); + // Habilitar/Deshabilitar podría ser un método separado o parte de UpdateAsync + Task<(bool Exito, string? Error)> CambiarEstadoHabilitadoAsync(int userId, bool habilitar, int idUsuarioModificador); + + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/PasswordHasherService.cs b/Backend/GestionIntegral.Api/Services/Usuarios/PasswordHasherService.cs similarity index 97% rename from Backend/GestionIntegral.Api/Services/PasswordHasherService.cs rename to Backend/GestionIntegral.Api/Services/Usuarios/PasswordHasherService.cs index a70fcaf..c8c6796 100644 --- a/Backend/GestionIntegral.Api/Services/PasswordHasherService.cs +++ b/Backend/GestionIntegral.Api/Services/Usuarios/PasswordHasherService.cs @@ -1,7 +1,7 @@ using System.Security.Cryptography; using System.Text; -namespace GestionIntegral.Api.Services +namespace GestionIntegral.Api.Services.Usuarios { public class PasswordHasherService { diff --git a/Backend/GestionIntegral.Api/Services/Usuarios/PerfilService.cs b/Backend/GestionIntegral.Api/Services/Usuarios/PerfilService.cs new file mode 100644 index 0000000..8a3b652 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Usuarios/PerfilService.cs @@ -0,0 +1,225 @@ +using GestionIntegral.Api.Data; // Para DbConnectionFactory +using GestionIntegral.Api.Data.Repositories.Usuarios; +using GestionIntegral.Api.Dtos.Usuarios; +using GestionIntegral.Api.Models.Usuarios; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; // Para IsolationLevel +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Usuarios +{ + public class PerfilService : IPerfilService + { + private readonly IPerfilRepository _perfilRepository; + private readonly DbConnectionFactory _connectionFactory; // Necesario para transacciones + private readonly ILogger _logger; + private readonly IPermisoRepository _permisoRepository; + + public PerfilService(IPerfilRepository perfilRepository, IPermisoRepository permisoRepository, DbConnectionFactory connectionFactory, ILogger logger) + { + _perfilRepository = perfilRepository; + _permisoRepository = permisoRepository; + _connectionFactory = connectionFactory; // Inyectar DbConnectionFactory + _logger = logger; + } + + private PerfilDto MapToDto(Perfil perfil) => new PerfilDto + { + Id = perfil.Id, + NombrePerfil = perfil.NombrePerfil, + Descripcion = perfil.Descripcion + }; + + public async Task> ObtenerTodosAsync(string? nombreFilter) + { + var perfiles = await _perfilRepository.GetAllAsync(nombreFilter); + return perfiles.Select(MapToDto); + } + + public async Task ObtenerPorIdAsync(int id) + { + var perfil = await _perfilRepository.GetByIdAsync(id); + return perfil == null ? null : MapToDto(perfil); + } + + public async Task<(PerfilDto? Perfil, string? Error)> CrearAsync(CreatePerfilDto createDto, int idUsuario) + { + if (await _perfilRepository.ExistsByNameAsync(createDto.NombrePerfil)) + { + return (null, "El nombre del perfil ya existe."); + } + + var nuevoPerfil = new Perfil { NombrePerfil = createDto.NombrePerfil, Descripcion = createDto.Descripcion }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var perfilCreado = await _perfilRepository.CreateAsync(nuevoPerfil, idUsuario, transaction); + if (perfilCreado == null) throw new DataException("La creación en el repositorio devolvió null."); + + transaction.Commit(); + _logger.LogInformation("Perfil ID {IdPerfil} creado por Usuario ID {IdUsuario}.", perfilCreado.Id, idUsuario); + return (MapToDto(perfilCreado), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback CrearAsync Perfil."); } + _logger.LogError(ex, "Error CrearAsync Perfil. Nombre: {NombrePerfil}", createDto.NombrePerfil); + return (null, $"Error interno al crear el perfil: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdatePerfilDto updateDto, int idUsuario) + { + // Verificar existencia ANTES de iniciar la transacción para evitar trabajo innecesario + var perfilExistente = await _perfilRepository.GetByIdAsync(id); + if (perfilExistente == null) return (false, "Perfil no encontrado."); + + if (await _perfilRepository.ExistsByNameAsync(updateDto.NombrePerfil, id)) + { + return (false, "El nombre del perfil ya existe para otro registro."); + } + + var perfilAActualizar = new Perfil { Id = id, NombrePerfil = updateDto.NombrePerfil, Descripcion = updateDto.Descripcion }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var actualizado = await _perfilRepository.UpdateAsync(perfilAActualizar, idUsuario, transaction); + if (!actualizado) + { + // El repositorio ahora lanza KeyNotFoundException si no lo encuentra DENTRO de la tx. + // Si devuelve false sin excepción, podría ser otro error. + throw new DataException("La operación de actualización no afectó ninguna fila o falló."); + } + transaction.Commit(); + _logger.LogInformation("Perfil ID {IdPerfil} actualizado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException knfex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback ActualizarAsync Perfil (KeyNotFound)."); } + _logger.LogWarning(knfex, "Intento de actualizar Perfil ID: {Id} no encontrado dentro de la transacción.", id); + return (false, "Perfil no encontrado."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback ActualizarAsync Perfil."); } + _logger.LogError(ex, "Error ActualizarAsync Perfil ID: {Id}", id); + return (false, $"Error interno al actualizar el perfil: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) + { + // Verificar existencia y si está en uso ANTES de la transacción + var perfilExistente = await _perfilRepository.GetByIdAsync(id); + if (perfilExistente == null) return (false, "Perfil no encontrado."); + + if (await _perfilRepository.IsInUseAsync(id)) + { + return (false, "No se puede eliminar. El perfil está asignado a usuarios o permisos."); + } + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var eliminado = await _perfilRepository.DeleteAsync(id, idUsuario, transaction); + if (!eliminado) + { + throw new DataException("La operación de eliminación no afectó ninguna fila o falló."); + } + transaction.Commit(); + _logger.LogInformation("Perfil ID {IdPerfil} eliminado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException knfex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback EliminarAsync Perfil (KeyNotFound)."); } + _logger.LogWarning(knfex, "Intento de eliminar Perfil ID: {Id} no encontrado dentro de la transacción.", id); + return (false, "Perfil no encontrado."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback EliminarAsync Perfil."); } + _logger.LogError(ex, "Error EliminarAsync Perfil ID: {Id}", id); + return (false, $"Error interno al eliminar el perfil: {ex.Message}"); + } + } + public async Task> ObtenerPermisosAsignadosAsync(int idPerfil) + { + // 1. Obtener todos los permisos definidos en el sistema + var todosLosPermisos = await _permisoRepository.GetAllAsync(null, null); // Sin filtros + + // 2. Obtener los IDs de los permisos actualmente asignados a este perfil + var idsPermisosAsignados = (await _perfilRepository.GetPermisoIdsByPerfilIdAsync(idPerfil)).ToHashSet(); + + // 3. Mapear a DTO, marcando 'Asignado' + var resultado = todosLosPermisos.Select(p => new PermisoAsignadoDto + { + Id = p.Id, + Modulo = p.Modulo, + DescPermiso = p.DescPermiso, + CodAcc = p.CodAcc, + Asignado = idsPermisosAsignados.Contains(p.Id) + }).OrderBy(p => p.Modulo).ThenBy(p => p.DescPermiso); // Ordenar para la UI + + return resultado; + } + + public async Task<(bool Exito, string? Error)> ActualizarPermisosAsignadosAsync( + int idPerfil, + ActualizarPermisosPerfilRequestDto request, + int idUsuarioModificador) + { + // Validación: Verificar que el perfil exista + var perfil = await _perfilRepository.GetByIdAsync(idPerfil); + if (perfil == null) + { + return (false, "Perfil no encontrado."); + } + + // Validación opcional: Verificar que todos los IDs de permisos en la solicitud sean válidos + if (request.PermisosIds != null && request.PermisosIds.Any()) + { + var permisosValidos = await _permisoRepository.GetPermisosByIdsAsync(request.PermisosIds); + if (permisosValidos.Count() != request.PermisosIds.Distinct().Count()) // Compara counts para detectar IDs inválidos + { + return (false, "Uno o más IDs de permisos proporcionados son inválidos."); + } + } + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + // El idUsuarioModificador no se usa directamente en UpdatePermisosByPerfilIdAsync + // porque no estamos grabando en una tabla _H para gral_PermisosPerfiles. + // Si se necesitara auditoría de esta acción específica, se debería añadir. + await _perfilRepository.UpdatePermisosByPerfilIdAsync(idPerfil, request.PermisosIds ?? new List(), transaction); + transaction.Commit(); + _logger.LogInformation("Permisos actualizados para Perfil ID {IdPerfil} por Usuario ID {IdUsuarioModificador}.", idPerfil, idUsuarioModificador); + return (true, null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback ActualizarPermisosAsignadosAsync."); } + _logger.LogError(ex, "Error al actualizar permisos para Perfil ID: {IdPerfil}", idPerfil); + return (false, $"Error interno al actualizar los permisos del perfil: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Usuarios/PermisoService.cs b/Backend/GestionIntegral.Api/Services/Usuarios/PermisoService.cs new file mode 100644 index 0000000..93625f7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Usuarios/PermisoService.cs @@ -0,0 +1,163 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Usuarios; +using GestionIntegral.Api.Dtos.Usuarios; +using GestionIntegral.Api.Models.Usuarios; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Usuarios +{ + public class PermisoService : IPermisoService + { + private readonly IPermisoRepository _permisoRepository; + private readonly DbConnectionFactory _connectionFactory; // Inyectar para transacciones + private readonly ILogger _logger; + + public PermisoService(IPermisoRepository permisoRepository, DbConnectionFactory connectionFactory, ILogger logger) + { + _permisoRepository = permisoRepository; + _connectionFactory = connectionFactory; // Asignar + _logger = logger; + } + + private PermisoDto MapToDto(Permiso permiso) => new PermisoDto + { + Id = permiso.Id, + Modulo = permiso.Modulo, + DescPermiso = permiso.DescPermiso, + CodAcc = permiso.CodAcc + }; + + public async Task> ObtenerTodosAsync(string? moduloFilter, string? codAccFilter) + { + var permisos = await _permisoRepository.GetAllAsync(moduloFilter, codAccFilter); + return permisos.Select(MapToDto); + } + + public async Task ObtenerPorIdAsync(int id) + { + var permiso = await _permisoRepository.GetByIdAsync(id); + return permiso == null ? null : MapToDto(permiso); + } + + public async Task<(PermisoDto? Permiso, string? Error)> CrearAsync(CreatePermisoDto createDto, int idUsuario) + { + if (await _permisoRepository.ExistsByCodAccAsync(createDto.CodAcc)) + { + return (null, "El código de acceso (CodAcc) ya existe."); + } + + var nuevoPermiso = new Permiso + { + Modulo = createDto.Modulo, + DescPermiso = createDto.DescPermiso, + CodAcc = createDto.CodAcc + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var permisoCreado = await _permisoRepository.CreateAsync(nuevoPermiso, idUsuario, transaction); + if (permisoCreado == null) throw new DataException("La creación en el repositorio devolvió null."); + + transaction.Commit(); + _logger.LogInformation("Permiso ID {IdPermiso} creado por Usuario ID {IdUsuario}.", permisoCreado.Id, idUsuario); + return (MapToDto(permisoCreado), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback CrearAsync Permiso."); } + _logger.LogError(ex, "Error CrearAsync Permiso. CodAcc: {CodAcc}", createDto.CodAcc); + return (null, $"Error interno al crear el permiso: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdatePermisoDto updateDto, int idUsuario) + { + var permisoExistente = await _permisoRepository.GetByIdAsync(id); // Verificar existencia fuera de la TX + if (permisoExistente == null) return (false, "Permiso no encontrado."); + + if (await _permisoRepository.ExistsByCodAccAsync(updateDto.CodAcc, id)) + { + return (false, "El código de acceso (CodAcc) ya existe para otro permiso."); + } + + var permisoAActualizar = new Permiso + { + Id = id, + Modulo = updateDto.Modulo, + DescPermiso = updateDto.DescPermiso, + CodAcc = updateDto.CodAcc + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var actualizado = await _permisoRepository.UpdateAsync(permisoAActualizar, idUsuario, transaction); + if (!actualizado) throw new DataException("La operación de actualización no afectó ninguna fila."); + + transaction.Commit(); + _logger.LogInformation("Permiso ID {IdPermiso} actualizado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException knfex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback ActualizarAsync Permiso (KeyNotFound)."); } + _logger.LogWarning(knfex, "Intento de actualizar Permiso ID: {Id} no encontrado dentro de la transacción.", id); + return (false, "Permiso no encontrado."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback ActualizarAsync Permiso."); } + _logger.LogError(ex, "Error ActualizarAsync Permiso ID: {Id}", id); + return (false, $"Error interno al actualizar el permiso: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) + { + var permisoExistente = await _permisoRepository.GetByIdAsync(id); // Verificar existencia fuera de la TX + if (permisoExistente == null) return (false, "Permiso no encontrado."); + + if (await _permisoRepository.IsInUseAsync(id)) + { + return (false, "No se puede eliminar. El permiso está asignado a uno o más perfiles."); + } + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var eliminado = await _permisoRepository.DeleteAsync(id, idUsuario, transaction); + if (!eliminado) throw new DataException("La operación de eliminación no afectó ninguna fila."); + + transaction.Commit(); + _logger.LogInformation("Permiso ID {IdPermiso} eliminado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException knfex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback EliminarAsync Permiso (KeyNotFound)."); } + _logger.LogWarning(knfex, "Intento de eliminar Permiso ID: {Id} no encontrado dentro de la transacción.", id); + return (false, "Permiso no encontrado."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback EliminarAsync Permiso."); } + _logger.LogError(ex, "Error EliminarAsync Permiso ID: {Id}", id); + return (false, $"Error interno al eliminar el permiso: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Usuarios/UsuarioService.cs b/Backend/GestionIntegral.Api/Services/Usuarios/UsuarioService.cs new file mode 100644 index 0000000..f9b0f38 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Usuarios/UsuarioService.cs @@ -0,0 +1,246 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Usuarios; +using GestionIntegral.Api.Dtos.Usuarios; +using GestionIntegral.Api.Models.Usuarios; // Para Usuario +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Usuarios +{ + public class UsuarioService : IUsuarioService + { + private readonly IUsuarioRepository _usuarioRepository; + private readonly IPerfilRepository _perfilRepository; + private readonly PasswordHasherService _passwordHasher; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public UsuarioService( + IUsuarioRepository usuarioRepository, + IPerfilRepository perfilRepository, + PasswordHasherService passwordHasher, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _usuarioRepository = usuarioRepository; + _perfilRepository = perfilRepository; + _passwordHasher = passwordHasher; + _connectionFactory = connectionFactory; + _logger = logger; + } + + // CORREGIDO: MapToDto ahora acepta una tupla con tipos anulables + private UsuarioDto? MapToDto((Usuario? usuario, string? nombrePerfil) data) + { + if (data.usuario == null) return null; // Si el usuario es null, no se puede mapear + + return new UsuarioDto + { + Id = data.usuario.Id, + User = data.usuario.User, + Habilitada = data.usuario.Habilitada, + SupAdmin = data.usuario.SupAdmin, + Nombre = data.usuario.Nombre, + Apellido = data.usuario.Apellido, + IdPerfil = data.usuario.IdPerfil, + NombrePerfil = data.nombrePerfil ?? "N/A", // Manejar null para nombrePerfil + DebeCambiarClave = data.usuario.DebeCambiarClave, + VerLog = data.usuario.VerLog + }; + } + + + public async Task> ObtenerTodosAsync(string? userFilter, string? nombreFilter) + { + var usuariosConPerfil = await _usuarioRepository.GetAllWithProfileNameAsync(userFilter, nombreFilter); + // Filtrar los nulos que MapToDto podría devolver (aunque no debería en este caso si GetAllWithProfileNameAsync no devuelve usuarios nulos en la tupla) + return usuariosConPerfil.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!); + } + + public async Task ObtenerPorIdAsync(int id) + { + var data = await _usuarioRepository.GetByIdWithProfileNameAsync(id); + // MapToDto ya maneja el caso donde data.Usuario es null y devuelve null. + return MapToDto(data); + } + + + public async Task<(UsuarioDto? Usuario, string? Error)> CrearAsync(CreateUsuarioRequestDto createDto, int idUsuarioCreador) + { + if (await _usuarioRepository.UserExistsAsync(createDto.User)) + { + return (null, "El nombre de usuario ya existe."); + } + var perfilSeleccionado = await _perfilRepository.GetByIdAsync(createDto.IdPerfil); + if (perfilSeleccionado == null) + { + return (null, "El perfil seleccionado no es válido."); + } + if(createDto.User.Equals(createDto.Password, System.StringComparison.OrdinalIgnoreCase)) + { + return (null, "La contraseña no puede ser igual al nombre de usuario."); + } + + (string hash, string salt) = _passwordHasher.HashPassword(createDto.Password); + + var nuevoUsuario = new Usuario + { + User = createDto.User, + ClaveHash = hash, + ClaveSalt = salt, + Habilitada = createDto.Habilitada, + SupAdmin = createDto.SupAdmin, + Nombre = createDto.Nombre, + Apellido = createDto.Apellido, + IdPerfil = createDto.IdPerfil, + VerLog = createDto.VerLog, + DebeCambiarClave = createDto.DebeCambiarClave + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + var usuarioCreado = await _usuarioRepository.CreateAsync(nuevoUsuario, idUsuarioCreador, transaction); + if (usuarioCreado == null) throw new DataException("Error al crear el usuario en el repositorio."); + + transaction.Commit(); + + // Construir el DTO para la respuesta + var dto = new UsuarioDto { + Id = usuarioCreado.Id, User = usuarioCreado.User, Habilitada = usuarioCreado.Habilitada, SupAdmin = usuarioCreado.SupAdmin, + Nombre = usuarioCreado.Nombre, Apellido = usuarioCreado.Apellido, IdPerfil = usuarioCreado.IdPerfil, + NombrePerfil = perfilSeleccionado.NombrePerfil, // Usamos el nombre del perfil ya obtenido + DebeCambiarClave = usuarioCreado.DebeCambiarClave, VerLog = usuarioCreado.VerLog + }; + _logger.LogInformation("Usuario ID {UsuarioId} creado por Usuario ID {CreadorId}.", usuarioCreado.Id, idUsuarioCreador); + return (dto, null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { /* Log */ } + _logger.LogError(ex, "Error CrearAsync Usuario. User: {User}", createDto.User); + return (null, $"Error interno al crear el usuario: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateUsuarioRequestDto updateDto, int idUsuarioModificador) + { + var usuarioExistente = await _usuarioRepository.GetByIdAsync(id); + if (usuarioExistente == null) return (false, "Usuario no encontrado."); + + if (await _perfilRepository.GetByIdAsync(updateDto.IdPerfil) == null) + { + return (false, "El perfil seleccionado no es válido."); + } + + usuarioExistente.Nombre = updateDto.Nombre; + usuarioExistente.Apellido = updateDto.Apellido; + usuarioExistente.IdPerfil = updateDto.IdPerfil; + usuarioExistente.Habilitada = updateDto.Habilitada; + usuarioExistente.SupAdmin = updateDto.SupAdmin; + usuarioExistente.DebeCambiarClave = updateDto.DebeCambiarClave; + usuarioExistente.VerLog = updateDto.VerLog; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + var actualizado = await _usuarioRepository.UpdateAsync(usuarioExistente, idUsuarioModificador, transaction); + if (!actualizado) throw new DataException("Error al actualizar el usuario en el repositorio."); + + transaction.Commit(); + _logger.LogInformation("Usuario ID {UsuarioId} actualizado por Usuario ID {ModificadorId}.", id, idUsuarioModificador); + return (true, null); + } + catch (KeyNotFoundException) { + try { transaction.Rollback(); } catch { /* Log */ } + return (false, "Usuario no encontrado durante la actualización."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { /* Log */ } + _logger.LogError(ex, "Error ActualizarAsync Usuario ID: {UsuarioId}", id); + return (false, $"Error interno al actualizar el usuario: {ex.Message}"); + } + } + public async Task<(bool Exito, string? Error)> SetPasswordAsync(int userId, SetPasswordRequestDto setPasswordDto, int idUsuarioModificador) + { + var usuario = await _usuarioRepository.GetByIdAsync(userId); + if (usuario == null) return (false, "Usuario no encontrado."); + + if(usuario.User.Equals(setPasswordDto.NewPassword, System.StringComparison.OrdinalIgnoreCase)) + { + return (false, "La nueva contraseña no puede ser igual al nombre de usuario."); + } + + (string hash, string salt) = _passwordHasher.HashPassword(setPasswordDto.NewPassword); + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var success = await _usuarioRepository.SetPasswordAsync(userId, hash, salt, setPasswordDto.ForceChangeOnNextLogin, idUsuarioModificador, transaction); + if(!success) throw new DataException("Error al actualizar la contraseña en el repositorio."); + + transaction.Commit(); + _logger.LogInformation("Contraseña establecida para Usuario ID {TargetUserId} por Usuario ID {AdminUserId}.", userId, idUsuarioModificador); + return (true, null); + } + catch (KeyNotFoundException) { + try { transaction.Rollback(); } catch { /* Log */ } + return (false, "Usuario no encontrado durante el cambio de contraseña."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { /* Log */ } + _logger.LogError(ex, "Error SetPasswordAsync para Usuario ID {TargetUserId}.", userId); + return (false, $"Error interno al establecer la contraseña: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> CambiarEstadoHabilitadoAsync(int userId, bool habilitar, int idUsuarioModificador) + { + var usuario = await _usuarioRepository.GetByIdAsync(userId); + if (usuario == null) return (false, "Usuario no encontrado."); + + if (usuario.Habilitada == habilitar) + { + return (true, null); // No hay cambio necesario + } + + usuario.Habilitada = habilitar; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(); + try + { + var actualizado = await _usuarioRepository.UpdateAsync(usuario, idUsuarioModificador, transaction); + if (!actualizado) throw new DataException("Error al cambiar estado de habilitación del usuario en el repositorio."); + + transaction.Commit(); + _logger.LogInformation("Estado de habilitación cambiado a {Estado} para Usuario ID {TargetUserId} por Usuario ID {AdminUserId}.", habilitar, userId, idUsuarioModificador); + return (true, null); + } + catch (KeyNotFoundException) { + try { transaction.Rollback(); } catch { /* Log */ } + return (false, "Usuario no encontrado durante el cambio de estado."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch { /* Log */ } + _logger.LogError(ex, "Error al cambiar estado de habilitación para Usuario ID {TargetUserId}.", userId); + return (false, $"Error interno al cambiar estado de habilitación: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/appsettings.Development.json b/Backend/GestionIntegral.Api/appsettings.Development.json index cbe1105..7ff9f69 100644 --- a/Backend/GestionIntegral.Api/appsettings.Development.json +++ b/Backend/GestionIntegral.Api/appsettings.Development.json @@ -9,7 +9,7 @@ "DefaultConnection": "Server=TECNICA3;Database=gestionvbnet;User ID=apigestion;Password=1351;Encrypt=False;TrustServerCertificate=True;" }, "Jwt": { - "Key": "ESTA_ES_UNA_CLAVE_SECRETA_MUY_LARGA_Y_SEGURA_CAMBIARLA!", + "Key": "badb1a38d221c9e23bcf70958840ca7f5a5dc54f2047dadf7ce45b578b5bc3e2", "Issuer": "GestionIntegralApi", "Audience": "GestionIntegralClient", "DurationInHours": 8 diff --git a/Backend/GestionIntegral.Api/bin/Debug/net9.0/appsettings.Development.json b/Backend/GestionIntegral.Api/bin/Debug/net9.0/appsettings.Development.json index cbe1105..7ff9f69 100644 --- a/Backend/GestionIntegral.Api/bin/Debug/net9.0/appsettings.Development.json +++ b/Backend/GestionIntegral.Api/bin/Debug/net9.0/appsettings.Development.json @@ -9,7 +9,7 @@ "DefaultConnection": "Server=TECNICA3;Database=gestionvbnet;User ID=apigestion;Password=1351;Encrypt=False;TrustServerCertificate=True;" }, "Jwt": { - "Key": "ESTA_ES_UNA_CLAVE_SECRETA_MUY_LARGA_Y_SEGURA_CAMBIARLA!", + "Key": "badb1a38d221c9e23bcf70958840ca7f5a5dc54f2047dadf7ce45b578b5bc3e2", "Issuer": "GestionIntegralApi", "Audience": "GestionIntegralClient", "DurationInHours": 8 diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs index f55c5a4..9b5f41b 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5c4b961073f79c4d61e3b3664349cf440b6ae4f4")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+daf84d27081c399bf1dbd5db88606c4b562cee46")] [assembly: System.Reflection.AssemblyProductAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.csproj.FileListAbsolute.txt b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.csproj.FileListAbsolute.txt index 85e9106..abac05b 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.csproj.FileListAbsolute.txt +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.csproj.FileListAbsolute.txt @@ -71,15 +71,10 @@ E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\scopedcss\bun E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\staticwebassets.build.json E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\staticwebassets.development.json E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\staticwebassets.build.endpoints.json -E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\staticwebassets\msbuild.GestionIntegral.Api.Microsoft.AspNetCore.StaticWebAssets.props -E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\staticwebassets\msbuild.GestionIntegral.Api.Microsoft.AspNetCore.StaticWebAssetEndpoints.props -E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\staticwebassets\msbuild.build.GestionIntegral.Api.props -E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\staticwebassets\msbuild.buildMultiTargeting.GestionIntegral.Api.props -E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\staticwebassets\msbuild.buildTransitive.GestionIntegral.Api.props -E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\staticwebassets.pack.json E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\GestionI.B26D474E.Up2Date E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\GestionIntegral.Api.dll E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\refint\GestionIntegral.Api.dll E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\GestionIntegral.Api.pdb E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\GestionIntegral.Api.genruntimeconfig.cache E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\ref\GestionIntegral.Api.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\obj\Debug\net9.0\staticwebassets.build.json.cache diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rbcswa.dswa.cache.json b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rbcswa.dswa.cache.json new file mode 100644 index 0000000..778d7b3 --- /dev/null +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rbcswa.dswa.cache.json @@ -0,0 +1 @@ +{"GlobalPropertiesHash":"2ilJ2M8+ZdH0swl4cXFj9Ji8kay0R08ISE/fEc+OL0o=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":[],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json new file mode 100644 index 0000000..6799a32 --- /dev/null +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json @@ -0,0 +1 @@ +{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","cInNtDBnkQU/n2nz4dl\u002BN2Fu06kTT6IORBeDdunxooU="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json new file mode 100644 index 0000000..9179fe7 --- /dev/null +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json @@ -0,0 +1 @@ +{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","cInNtDBnkQU/n2nz4dl\u002BN2Fu06kTT6IORBeDdunxooU="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rpswa.dswa.cache.json b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rpswa.dswa.cache.json new file mode 100644 index 0000000..c6a20dd --- /dev/null +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rpswa.dswa.cache.json @@ -0,0 +1 @@ +{"GlobalPropertiesHash":"nueagD6vos1qa5Z6EdwL+uix/UGN3umfwM2JskZDeIQ=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.build.GestionIntegral.Api.props b/Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.build.GestionIntegral.Api.props deleted file mode 100644 index ddaed44..0000000 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.build.GestionIntegral.Api.props +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.buildMultiTargeting.GestionIntegral.Api.props b/Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.buildMultiTargeting.GestionIntegral.Api.props deleted file mode 100644 index f20b95b..0000000 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.buildMultiTargeting.GestionIntegral.Api.props +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.buildTransitive.GestionIntegral.Api.props b/Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.buildTransitive.GestionIntegral.Api.props deleted file mode 100644 index b5c6269..0000000 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/staticwebassets/msbuild.buildTransitive.GestionIntegral.Api.props +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.dgspec.json b/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.dgspec.json index 0e18234..d648775 100644 --- a/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.dgspec.json +++ b/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.dgspec.json @@ -1,20 +1,20 @@ { "format": 1, "restore": { - "E:\\GestionIntegralWeb\\backend\\gestionintegral.api\\GestionIntegral.Api.csproj": {} + "E:\\GestionIntegralWeb\\Backend\\GestionIntegral.Api\\GestionIntegral.Api.csproj": {} }, "projects": { - "E:\\GestionIntegralWeb\\backend\\gestionintegral.api\\GestionIntegral.Api.csproj": { + "E:\\GestionIntegralWeb\\Backend\\GestionIntegral.Api\\GestionIntegral.Api.csproj": { "version": "1.0.0", "restore": { - "projectUniqueName": "E:\\GestionIntegralWeb\\backend\\gestionintegral.api\\GestionIntegral.Api.csproj", + "projectUniqueName": "E:\\GestionIntegralWeb\\Backend\\GestionIntegral.Api\\GestionIntegral.Api.csproj", "projectName": "GestionIntegral.Api", - "projectPath": "E:\\GestionIntegralWeb\\backend\\gestionintegral.api\\GestionIntegral.Api.csproj", + "projectPath": "E:\\GestionIntegralWeb\\Backend\\GestionIntegral.Api\\GestionIntegral.Api.csproj", "packagesPath": "C:\\Users\\dmolinari\\.nuget\\packages\\", - "outputPath": "E:\\GestionIntegralWeb\\backend\\gestionintegral.api\\obj\\", + "outputPath": "E:\\GestionIntegralWeb\\Backend\\GestionIntegral.Api\\obj\\", "projectStyle": "PackageReference", "fallbackFolders": [ - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" + "D:\\Microsoft\\VisualStudio\\Microsoft Visual Studio\\Shared\\NuGetPackages" ], "configFilePaths": [ "C:\\Users\\dmolinari\\AppData\\Roaming\\NuGet\\NuGet.Config", @@ -26,7 +26,6 @@ ], "sources": { "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, - "C:\\Program Files\\dotnet\\library-packs": {}, "https://api.nuget.org/v3/index.json": {} }, "frameworks": { @@ -45,7 +44,7 @@ "auditLevel": "low", "auditMode": "direct" }, - "SdkAnalysisLevel": "9.0.200" + "SdkAnalysisLevel": "9.0.300" }, "frameworks": { "net9.0": { @@ -95,7 +94,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.201/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.300/PortableRuntimeIdentifierGraph.json" } } } diff --git a/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.g.props b/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.g.props index 34c9e06..2652469 100644 --- a/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.g.props +++ b/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.g.props @@ -5,13 +5,13 @@ NuGet $(MSBuildThisFileDirectory)project.assets.json $(UserProfile)\.nuget\packages\ - C:\Users\dmolinari\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages + C:\Users\dmolinari\.nuget\packages\;D:\Microsoft\VisualStudio\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.13.1 + 6.14.0 - + diff --git a/Backend/GestionIntegral.Api/obj/project.assets.json b/Backend/GestionIntegral.Api/obj/project.assets.json index 2555478..f51c9f3 100644 --- a/Backend/GestionIntegral.Api/obj/project.assets.json +++ b/Backend/GestionIntegral.Api/obj/project.assets.json @@ -2170,7 +2170,7 @@ }, "packageFolders": { "C:\\Users\\dmolinari\\.nuget\\packages\\": {}, - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {} + "D:\\Microsoft\\VisualStudio\\Microsoft Visual Studio\\Shared\\NuGetPackages": {} }, "project": { "version": "1.0.0", @@ -2182,7 +2182,7 @@ "outputPath": "E:\\GestionIntegralWeb\\Backend\\GestionIntegral.Api\\obj\\", "projectStyle": "PackageReference", "fallbackFolders": [ - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" + "D:\\Microsoft\\VisualStudio\\Microsoft Visual Studio\\Shared\\NuGetPackages" ], "configFilePaths": [ "C:\\Users\\dmolinari\\AppData\\Roaming\\NuGet\\NuGet.Config", @@ -2194,7 +2194,6 @@ ], "sources": { "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, - "C:\\Program Files\\dotnet\\library-packs": {}, "https://api.nuget.org/v3/index.json": {} }, "frameworks": { @@ -2213,7 +2212,7 @@ "auditLevel": "low", "auditMode": "direct" }, - "SdkAnalysisLevel": "9.0.200" + "SdkAnalysisLevel": "9.0.300" }, "frameworks": { "net9.0": { @@ -2263,7 +2262,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.201/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.300/PortableRuntimeIdentifierGraph.json" } } } diff --git a/Frontend/src/components/Modals/TipoPagoFormModal.tsx b/Frontend/src/components/Modals/Contables/TipoPagoFormModal.tsx similarity index 96% rename from Frontend/src/components/Modals/TipoPagoFormModal.tsx rename to Frontend/src/components/Modals/Contables/TipoPagoFormModal.tsx index 9e69f04..537b13e 100644 --- a/Frontend/src/components/Modals/TipoPagoFormModal.tsx +++ b/Frontend/src/components/Modals/Contables/TipoPagoFormModal.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; -import type { TipoPago } from '../../models/Entities/TipoPago'; -import type { CreateTipoPagoDto } from '../../models/dtos/tiposPago/CreateTipoPagoDto'; +import type { TipoPago } from '../../../models/Entities/TipoPago'; +import type { CreateTipoPagoDto } from '../../../models/dtos/tiposPago/CreateTipoPagoDto'; const modalStyle = { position: 'absolute' as 'absolute', diff --git a/Frontend/src/components/Modals/Distribucion/CanillaFormModal.tsx b/Frontend/src/components/Modals/Distribucion/CanillaFormModal.tsx new file mode 100644 index 0000000..900c7b8 --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/CanillaFormModal.tsx @@ -0,0 +1,239 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox +} from '@mui/material'; +import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; +import type { CreateCanillaDto } from '../../../models/dtos/Distribucion/CreateCanillaDto'; +import type { UpdateCanillaDto } from '../../../models/dtos/Distribucion/UpdateCanillaDto'; +import type { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto'; // Para el dropdown de zonas +import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto'; // Para el dropdown de empresas +import zonaService from '../../../services/Distribucion/zonaService'; +import empresaService from '../../../services/Distribucion/empresaService'; + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 600 }, // Un poco más ancho + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface CanillaFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateCanillaDto | UpdateCanillaDto, id?: number) => Promise; + initialData?: CanillaDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const CanillaFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [legajo, setLegajo] = useState(''); // Manejar como string para el TextField + const [nomApe, setNomApe] = useState(''); + const [parada, setParada] = useState(''); + const [idZona, setIdZona] = useState(''); + const [accionista, setAccionista] = useState(false); + const [obs, setObs] = useState(''); + const [empresa, setEmpresa] = useState(0); // 0 para N/A (accionista) + + const [zonas, setZonas] = useState([]); + const [empresas, setEmpresas] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchDropdownData = async () => { + setLoadingDropdowns(true); + try { + const [zonasData, empresasData] = await Promise.all([ + zonaService.getAllZonas(), // Asume que este servicio devuelve zonas activas + empresaService.getAllEmpresas() + ]); + setZonas(zonasData); + setEmpresas(empresasData); + } catch (error) { + console.error("Error al cargar datos para dropdowns", error); + setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar zonas/empresas.'})); + } finally { + setLoadingDropdowns(false); + } + }; + + if (open) { + fetchDropdownData(); + setLegajo(initialData?.legajo?.toString() || ''); + setNomApe(initialData?.nomApe || ''); + setParada(initialData?.parada || ''); + setIdZona(initialData?.idZona || ''); + setAccionista(initialData ? initialData.accionista : false); + setObs(initialData?.obs || ''); + setEmpresa(initialData ? initialData.empresa : 0); // Si es accionista, empresa es 0 + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!nomApe.trim()) errors.nomApe = 'Nombre y Apellido son obligatorios.'; + if (!idZona) errors.idZona = 'Debe seleccionar una zona.'; + + const legajoNum = legajo ? parseInt(legajo, 10) : null; + if (legajo.trim() && (isNaN(legajoNum!) || legajoNum! < 0)) { + errors.legajo = 'Legajo debe ser un número positivo o vacío.'; + } + + // Lógica de empresa y accionista + if (accionista && empresa !== 0 && empresa !== '') { + errors.empresa = 'Si es Accionista, la Empresa debe ser N/A (0).'; + } + if (!accionista && (empresa === 0 || empresa === '')) { + errors.empresa = 'Si no es Accionista, debe seleccionar una Empresa.'; + } + + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) { + setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + } + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + const legajoParsed = legajo.trim() ? parseInt(legajo, 10) : null; + + const dataToSubmit = { + legajo: legajoParsed, + nomApe, + parada: parada || undefined, + idZona: Number(idZona), + accionista, + obs: obs || undefined, + empresa: Number(empresa) + }; + + if (isEditing && initialData) { + await onSubmit(dataToSubmit as UpdateCanillaDto, initialData.idCanilla); + } else { + await onSubmit(dataToSubmit as CreateCanillaDto); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de CanillaFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Canillita' : 'Agregar Nuevo Canillita'} + + + {/* SECCIÓN DE CAMPOS CON BOX Y FLEXBOX */} + + + {setLegajo(e.target.value); handleInputChange('legajo');}} + margin="dense" fullWidth error={!!localErrors.legajo} helperText={localErrors.legajo || ''} + disabled={loading} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} + /> + {setNomApe(e.target.value); handleInputChange('nomApe');}} + margin="dense" fullWidth error={!!localErrors.nomApe} helperText={localErrors.nomApe || ''} + disabled={loading} autoFocus={!isEditing} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} + /> + + setParada(e.target.value)} + margin="dense" fullWidth multiline rows={2} disabled={loading} + /> + + + Zona + + {localErrors.idZona && {localErrors.idZona}} + + + Empresa + + {localErrors.empresa && {localErrors.empresa}} + + + setObs(e.target.value)} + margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{mt:1}} + /> + + { + setAccionista(e.target.checked); + if (e.target.checked) setEmpresa(0); + handleInputChange('accionista'); + handleInputChange('empresa'); + }} disabled={loading}/>} + label="Es Accionista" + /> + + + + {errorMessage && {errorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + + + + ); +}; + +export default CanillaFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/DistribuidorFormModal.tsx b/Frontend/src/components/Modals/Distribucion/DistribuidorFormModal.tsx new file mode 100644 index 0000000..1010ad4 --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/DistribuidorFormModal.tsx @@ -0,0 +1,239 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem +} from '@mui/material'; +import type { DistribuidorDto } from '../../../models/dtos/Distribucion/DistribuidorDto'; +import type { CreateDistribuidorDto } from '../../../models/dtos/Distribucion/CreateDistribuidorDto'; +import type { UpdateDistribuidorDto } from '../../../models/dtos/Distribucion/UpdateDistribuidorDto'; +import type { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto'; +import zonaService from '../../../services/Distribucion/zonaService'; + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '95%', sm: '80%', md: '600px' }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface DistribuidorFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateDistribuidorDto | UpdateDistribuidorDto, id?: number) => Promise; + initialData?: DistribuidorDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const DistribuidorFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [nombre, setNombre] = useState(''); + const [contacto, setContacto] = useState(''); + const [nroDoc, setNroDoc] = useState(''); + const [idZona, setIdZona] = useState(''); // Puede ser string vacío + const [calle, setCalle] = useState(''); + const [numero, setNumero] = useState(''); + const [piso, setPiso] = useState(''); + const [depto, setDepto] = useState(''); + const [telefono, setTelefono] = useState(''); + const [email, setEmail] = useState(''); + const [localidad, setLocalidad] = useState(''); + + const [zonas, setZonas] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingZonas, setLoadingZonas] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchZonas = async () => { + setLoadingZonas(true); + try { + const data = await zonaService.getAllZonas(); // Solo activas por defecto + setZonas(data); + } catch (error) { + console.error("Error al cargar zonas", error); + setLocalErrors(prev => ({...prev, zonas: 'Error al cargar zonas.'})); + } finally { + setLoadingZonas(false); + } + }; + + if (open) { + fetchZonas(); + setNombre(initialData?.nombre || ''); + setContacto(initialData?.contacto || ''); + setNroDoc(initialData?.nroDoc || ''); + setIdZona(initialData?.idZona || ''); + setCalle(initialData?.calle || ''); + setNumero(initialData?.numero || ''); + setPiso(initialData?.piso || ''); + setDepto(initialData?.depto || ''); + setTelefono(initialData?.telefono || ''); + setEmail(initialData?.email || ''); + setLocalidad(initialData?.localidad || ''); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!nombre.trim()) errors.nombre = 'El nombre es obligatorio.'; + if (!nroDoc.trim()) errors.nroDoc = 'El Nro. Documento es obligatorio.'; + else if (nroDoc.trim().length > 11) errors.nroDoc = 'El Nro. Documento no debe exceder los 11 caracteres.'; + + if (email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + errors.email = 'Formato de email inválido.'; + } + // No es obligatorio que IdZona tenga valor + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) { + setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + } + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + const dataToSubmit = { + nombre, + contacto: contacto || undefined, + nroDoc, + idZona: idZona ? Number(idZona) : null, // Enviar null si está vacío + calle: calle || undefined, + numero: numero || undefined, + piso: piso || undefined, + depto: depto || undefined, + telefono: telefono || undefined, + email: email || undefined, + localidad: localidad || undefined, + }; + + if (isEditing && initialData) { + await onSubmit(dataToSubmit as UpdateDistribuidorDto, initialData.idDistribuidor); + } else { + await onSubmit(dataToSubmit as CreateDistribuidorDto); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de DistribuidorFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Distribuidor' : 'Agregar Nuevo Distribuidor'} + + + {/* Usando Box con Flexbox para layout de dos columnas responsivo */} + + + {setNombre(e.target.value); handleInputChange('nombre');}} + margin="dense" fullWidth error={!!localErrors.nombre} helperText={localErrors.nombre || ''} + disabled={loading} autoFocus={!isEditing} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} // -8px es la mitad del gap + /> + {setNroDoc(e.target.value); handleInputChange('nroDoc');}} + margin="dense" fullWidth error={!!localErrors.nroDoc} helperText={localErrors.nroDoc || ''} + disabled={loading} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} + /> + + + setContacto(e.target.value)} + margin="dense" fullWidth disabled={loading} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} + /> + + Zona (Opcional) + + + + + setCalle(e.target.value)} margin="dense" + sx={{ flex: 3, minWidth: 'calc(60% - 8px)'}} // Más ancho para la calle + disabled={loading} + /> + setNumero(e.target.value)} margin="dense" + sx={{ flex: 1, minWidth: 'calc(40% - 8px)'}} + disabled={loading} + /> + + + setPiso(e.target.value)} margin="dense" + sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}} + disabled={loading} + /> + setDepto(e.target.value)} margin="dense" + sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}} + disabled={loading} + /> + + + setTelefono(e.target.value)} margin="dense" + sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}} + disabled={loading} + /> + {setEmail(e.target.value); handleInputChange('email');}} + margin="dense" fullWidth error={!!localErrors.email} helperText={localErrors.email || ''} + disabled={loading} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} + /> + + setLocalidad(e.target.value)} margin="dense" fullWidth disabled={loading} sx={{mt:1}}/> + + + {errorMessage && {errorMessage}} + {localErrors.zonas && {localErrors.zonas}} + + + + + + + + + ); +}; + +export default DistribuidorFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/EmpresaFormModal.tsx b/Frontend/src/components/Modals/Distribucion/EmpresaFormModal.tsx similarity index 94% rename from Frontend/src/components/Modals/EmpresaFormModal.tsx rename to Frontend/src/components/Modals/Distribucion/EmpresaFormModal.tsx index 7de03dd..2396547 100644 --- a/Frontend/src/components/Modals/EmpresaFormModal.tsx +++ b/Frontend/src/components/Modals/Distribucion/EmpresaFormModal.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from 'react'; import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; -import type { EmpresaDto } from '../../models/dtos/Empresas/EmpresaDto'; -import type { CreateEmpresaDto } from '../../models/dtos/Empresas/CreateEmpresaDto'; -import type { UpdateEmpresaDto } from '../../models/dtos/Empresas/UpdateEmpresaDto'; // Necesitamos Update DTO también +import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto'; +import type { CreateEmpresaDto } from '../../../models/dtos/Distribucion/CreateEmpresaDto'; +import type { UpdateEmpresaDto } from '../../../models/dtos/Distribucion/UpdateEmpresaDto'; // Necesitamos Update DTO también const modalStyle = { position: 'absolute' as 'absolute', diff --git a/Frontend/src/components/Modals/Distribucion/OtroDestinoFormModal.tsx b/Frontend/src/components/Modals/Distribucion/OtroDestinoFormModal.tsx new file mode 100644 index 0000000..0836889 --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/OtroDestinoFormModal.tsx @@ -0,0 +1,128 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; +import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto'; +import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto'; +import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto'; + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +interface OtroDestinoFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateOtroDestinoDto | (UpdateOtroDestinoDto & { idDestino: number })) => Promise; + initialData?: OtroDestinoDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const OtroDestinoFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [nombre, setNombre] = useState(''); + const [obs, setObs] = useState(''); + const [loading, setLoading] = useState(false); + const [localErrorNombre, setLocalErrorNombre] = useState(null); + + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + setNombre(initialData?.nombre || ''); + setObs(initialData?.obs || ''); + setLocalErrorNombre(null); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const handleInputChange = () => { + if (localErrorNombre) setLocalErrorNombre(null); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setLocalErrorNombre(null); + clearErrorMessage(); + + if (!nombre.trim()) { + setLocalErrorNombre('El nombre es obligatorio.'); + return; + } + + setLoading(true); + try { + const dataToSubmit = { nombre, obs: obs || undefined }; + + if (isEditing && initialData) { + await onSubmit({ ...dataToSubmit, idDestino: initialData.idDestino }); + } else { + await onSubmit(dataToSubmit as CreateOtroDestinoDto); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de OtroDestinoFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Otro Destino' : 'Agregar Nuevo Destino'} + + + { setNombre(e.target.value); handleInputChange(); }} + margin="normal" + error={!!localErrorNombre} + helperText={localErrorNombre || ''} + disabled={loading} + autoFocus + /> + setObs(e.target.value)} + margin="normal" + multiline + rows={3} + disabled={loading} + /> + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default OtroDestinoFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/PrecioFormModal.tsx b/Frontend/src/components/Modals/Distribucion/PrecioFormModal.tsx new file mode 100644 index 0000000..2329d1e --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/PrecioFormModal.tsx @@ -0,0 +1,225 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + InputAdornment + // Quitar Grid si no se usa +} from '@mui/material'; +import type { PrecioDto } from '../../../models/dtos/Distribucion/PrecioDto'; +import type { CreatePrecioDto } from '../../../models/dtos/Distribucion/CreatePrecioDto'; +import type { UpdatePrecioDto } from '../../../models/dtos/Distribucion/UpdatePrecioDto'; + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '95%', sm: '80%', md: '700px' }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +const diasSemana = ["Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado", "Domingo"] as const; +type DiaSemana = typeof diasSemana[number]; + +interface PrecioFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreatePrecioDto | UpdatePrecioDto, idPrecio?: number) => Promise; + idPublicacion: number; + initialData?: PrecioDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const PrecioFormModal: React.FC = ({ + open, + onClose, + onSubmit, + idPublicacion, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [vigenciaD, setVigenciaD] = useState(''); + const [vigenciaH, setVigenciaH] = useState(''); + const [preciosDia, setPreciosDia] = useState>({ + Lunes: '', Martes: '', Miercoles: '', Jueves: '', Viernes: '', Sabado: '', Domingo: '' + }); + + const [loading, setLoading] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + setVigenciaD(initialData?.vigenciaD || ''); + setVigenciaH(initialData?.vigenciaH || ''); + const initialPrecios: Record = { Lunes: '', Martes: '', Miercoles: '', Jueves: '', Viernes: '', Sabado: '', Domingo: '' }; + if (initialData) { + diasSemana.forEach(dia => { + const key = dia.toLowerCase() as keyof Omit; + initialPrecios[dia] = initialData[key]?.toString() || ''; + }); + } + setPreciosDia(initialPrecios); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!isEditing && !vigenciaD.trim()) { + errors.vigenciaD = 'La Vigencia Desde es obligatoria.'; + } else if (vigenciaD.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaD)) { + errors.vigenciaD = 'Formato de fecha inválido (YYYY-MM-DD).'; + } + + if (vigenciaH.trim() && !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaH)) { + errors.vigenciaH = 'Formato de fecha inválido (YYYY-MM-DD).'; + } else if (vigenciaH.trim() && vigenciaD.trim() && new Date(vigenciaH) < new Date(vigenciaD)) { + errors.vigenciaH = 'Vigencia Hasta no puede ser anterior a Vigencia Desde.'; + } + + let alMenosUnPrecio = false; + diasSemana.forEach(dia => { + const valor = preciosDia[dia]; + if (valor.trim()) { + alMenosUnPrecio = true; + if (isNaN(parseFloat(valor)) || parseFloat(valor) < 0) { + errors[dia.toLowerCase()] = `Precio de ${dia} inválido.`; + } + } + }); + if (!isEditing && !alMenosUnPrecio) { + errors.dias = 'Debe ingresar al menos un precio para un día de la semana.'; + } + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handlePrecioDiaChange = (dia: DiaSemana, value: string) => { + setPreciosDia(prev => ({ ...prev, [dia]: value })); + if (localErrors[dia.toLowerCase()] || localErrors.dias) { + setLocalErrors(prev => ({ ...prev, [dia.toLowerCase()]: null, dias: null })); + } + if (errorMessage) clearErrorMessage(); + }; + const handleDateChange = (setter: React.Dispatch>, fieldName: string) => (e: React.ChangeEvent) => { + setter(e.target.value); + if (localErrors[fieldName]) { + setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + } + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + const preciosNumericos: Partial, number | null>> = {}; + diasSemana.forEach(dia => { + const valor = preciosDia[dia].trim(); + // Convertir el nombre del día a la clave correcta del DTO (ej. "Lunes" -> "lunes") + const key = dia.toLowerCase() as keyof typeof preciosNumericos; + preciosNumericos[key] = valor ? parseFloat(valor) : null; + }); + + + if (isEditing && initialData) { + const dataToSubmit: UpdatePrecioDto = { + vigenciaH: vigenciaH.trim() ? vigenciaH : null, + ...preciosNumericos // Spread de los precios de los días + }; + await onSubmit(dataToSubmit, initialData.idPrecio); + } else { + const dataToSubmit: CreatePrecioDto = { + idPublicacion, + vigenciaD, + ...preciosNumericos + }; + await onSubmit(dataToSubmit); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de PrecioFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Período de Precio' : 'Agregar Nuevo Período de Precio'} + + + {/* Sección de Vigencias */} + + + {isEditing && ( + + )} + + + {/* Sección de Precios por Día con Flexbox */} + Precios por Día: + + {diasSemana.map(dia => ( + + {/* El ajuste de -Xpx en flexBasis es aproximado para compensar el gap. + Para 3 columnas (33.33%): gap es 16px, se distribuye entre 2 espacios -> 16/2 * 2/3 = ~11px + Para 4 columnas (25%): gap es 16px, se distribuye entre 3 espacios -> 16/3 * 3/4 = 12px + */} + handlePrecioDiaChange(dia, e.target.value)} + margin="dense" fullWidth + error={!!localErrors[dia.toLowerCase()]} helperText={localErrors[dia.toLowerCase()] || ''} + disabled={loading} + InputProps={{ startAdornment: $ }} + inputProps={{ step: "0.01", lang:"es-AR" }} + /> + + ))} + + {localErrors.dias && {localErrors.dias}} + + + {errorMessage && {errorMessage}} + + + + + + + + + ); +}; + +export default PrecioFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/PublicacionFormModal.tsx b/Frontend/src/components/Modals/Distribucion/PublicacionFormModal.tsx new file mode 100644 index 0000000..0b7111a --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/PublicacionFormModal.tsx @@ -0,0 +1,180 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox +} from '@mui/material'; +import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; +import type { CreatePublicacionDto } from '../../../models/dtos/Distribucion/CreatePublicacionDto'; +import type { UpdatePublicacionDto } from '../../../models/dtos/Distribucion/UpdatePublicacionDto'; +import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto'; +import empresaService from '../../../services/Distribucion/empresaService'; // Para cargar empresas + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 500 }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface PublicacionFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreatePublicacionDto | UpdatePublicacionDto, id?: number) => Promise; + initialData?: PublicacionDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const PublicacionFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [nombre, setNombre] = useState(''); + const [observacion, setObservacion] = useState(''); + const [idEmpresa, setIdEmpresa] = useState(''); + const [ctrlDevoluciones, setCtrlDevoluciones] = useState(false); + const [habilitada, setHabilitada] = useState(true); + + const [empresas, setEmpresas] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingEmpresas, setLoadingEmpresas] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchEmpresas = async () => { + setLoadingEmpresas(true); + try { + const data = await empresaService.getAllEmpresas(); + setEmpresas(data); + } catch (error) { + console.error("Error al cargar empresas", error); + setLocalErrors(prev => ({...prev, empresas: 'Error al cargar empresas.'})); + } finally { + setLoadingEmpresas(false); + } + }; + + if (open) { + fetchEmpresas(); + setNombre(initialData?.nombre || ''); + setObservacion(initialData?.observacion || ''); + setIdEmpresa(initialData?.idEmpresa || ''); + setCtrlDevoluciones(initialData ? initialData.ctrlDevoluciones : false); + setHabilitada(initialData ? initialData.habilitada : true); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!nombre.trim()) errors.nombre = 'El nombre es obligatorio.'; + if (!idEmpresa) errors.idEmpresa = 'Debe seleccionar una empresa.'; + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) { + setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + } + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + const dataToSubmit = { + nombre, + observacion: observacion || undefined, + idEmpresa: Number(idEmpresa), + ctrlDevoluciones, + habilitada + }; + + if (isEditing && initialData) { + await onSubmit(dataToSubmit as UpdatePublicacionDto, initialData.idPublicacion); + } else { + await onSubmit(dataToSubmit as CreatePublicacionDto); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de PublicacionFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Publicación' : 'Agregar Nueva Publicación'} + + + + {setNombre(e.target.value); handleInputChange('nombre');}} + margin="dense" fullWidth error={!!localErrors.nombre} helperText={localErrors.nombre || ''} + disabled={loading} autoFocus={!isEditing} + /> + + Empresa + + {localErrors.idEmpresa && {localErrors.idEmpresa}} + + setObservacion(e.target.value)} + margin="dense" fullWidth multiline rows={3} disabled={loading} + /> + + setCtrlDevoluciones(e.target.checked)} disabled={loading}/>} + label="Controla Devoluciones" + /> + setHabilitada(e.target.checked)} disabled={loading}/>} + label="Habilitada" + /> + + + + {errorMessage && {errorMessage}} + {localErrors.empresas && {localErrors.empresas}} + + + + + + + + + ); +}; + +export default PublicacionFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/ZonaFormModal.tsx b/Frontend/src/components/Modals/Distribucion/ZonaFormModal.tsx similarity index 95% rename from Frontend/src/components/Modals/ZonaFormModal.tsx rename to Frontend/src/components/Modals/Distribucion/ZonaFormModal.tsx index aa85342..346fb8d 100644 --- a/Frontend/src/components/Modals/ZonaFormModal.tsx +++ b/Frontend/src/components/Modals/Distribucion/ZonaFormModal.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; -import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // Usamos el DTO de la API para listar -import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; +import type { ZonaDto } from '../../../models/dtos/Zonas/ZonaDto'; // Usamos el DTO de la API para listar +import type { CreateZonaDto } from '../../../models/dtos/Zonas/CreateZonaDto'; const modalStyle = { position: 'absolute' as 'absolute', diff --git a/Frontend/src/components/Modals/EstadoBobinaFormModal.tsx b/Frontend/src/components/Modals/Impresion/EstadoBobinaFormModal.tsx similarity index 93% rename from Frontend/src/components/Modals/EstadoBobinaFormModal.tsx rename to Frontend/src/components/Modals/Impresion/EstadoBobinaFormModal.tsx index bb9018f..cace04d 100644 --- a/Frontend/src/components/Modals/EstadoBobinaFormModal.tsx +++ b/Frontend/src/components/Modals/Impresion/EstadoBobinaFormModal.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from 'react'; import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; -import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; -import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto'; -import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto'; +import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto'; +import type { CreateEstadoBobinaDto } from '../../../models/dtos/Impresion/CreateEstadoBobinaDto'; +import type { UpdateEstadoBobinaDto } from '../../../models/dtos/Impresion/UpdateEstadoBobinaDto'; const modalStyle = { position: 'absolute' as 'absolute', diff --git a/Frontend/src/components/Modals/PlantaFormModal.tsx b/Frontend/src/components/Modals/Impresion/PlantaFormModal.tsx similarity index 94% rename from Frontend/src/components/Modals/PlantaFormModal.tsx rename to Frontend/src/components/Modals/Impresion/PlantaFormModal.tsx index fc8c37f..5eccfcd 100644 --- a/Frontend/src/components/Modals/PlantaFormModal.tsx +++ b/Frontend/src/components/Modals/Impresion/PlantaFormModal.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from 'react'; import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; -import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; -import type { CreatePlantaDto } from '../../models/dtos/Impresion/CreatePlantaDto'; -import type { UpdatePlantaDto } from '../../models/dtos/Impresion/UpdatePlantaDto'; +import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto'; +import type { CreatePlantaDto } from '../../../models/dtos/Impresion/CreatePlantaDto'; +import type { UpdatePlantaDto } from '../../../models/dtos/Impresion/UpdatePlantaDto'; const modalStyle = { /* ... (mismo estilo que otros modales) ... */ position: 'absolute' as 'absolute', diff --git a/Frontend/src/components/Modals/TipoBobinaFormModal.tsx b/Frontend/src/components/Modals/Impresion/TipoBobinaFormModal.tsx similarity index 92% rename from Frontend/src/components/Modals/TipoBobinaFormModal.tsx rename to Frontend/src/components/Modals/Impresion/TipoBobinaFormModal.tsx index 6798867..6e3ffea 100644 --- a/Frontend/src/components/Modals/TipoBobinaFormModal.tsx +++ b/Frontend/src/components/Modals/Impresion/TipoBobinaFormModal.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from 'react'; import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; -import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; -import type { CreateTipoBobinaDto } from '../../models/dtos/Impresion/CreateTipoBobinaDto'; -import type { UpdateTipoBobinaDto } from '../../models/dtos/Impresion/UpdateTipoBobinaDto'; +import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto'; +import type { CreateTipoBobinaDto } from '../../../models/dtos/Impresion/CreateTipoBobinaDto'; +import type { UpdateTipoBobinaDto } from '../../../models/dtos/Impresion/UpdateTipoBobinaDto'; const modalStyle = { /* ... (mismo estilo que otros modales) ... */ position: 'absolute' as 'absolute', diff --git a/Frontend/src/components/ChangePasswordModal.tsx b/Frontend/src/components/Modals/Usuarios/ChangePasswordModal.tsx similarity index 96% rename from Frontend/src/components/ChangePasswordModal.tsx rename to Frontend/src/components/Modals/Usuarios/ChangePasswordModal.tsx index 19ff46b..9c77d6c 100644 --- a/Frontend/src/components/ChangePasswordModal.tsx +++ b/Frontend/src/components/Modals/Usuarios/ChangePasswordModal.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; -import { useAuth } from '../contexts/AuthContext'; -import authService from '../services/authService'; -import type { ChangePasswordRequestDto } from '../models/dtos/ChangePasswordRequestDto'; +import { useAuth } from '../../../contexts/AuthContext'; +import authService from '../../../services/Usuarios/authService'; +import type { ChangePasswordRequestDto } from '../../../models/dtos/Usuarios/ChangePasswordRequestDto'; import axios from 'axios'; import { Modal, Box, Typography, TextField, Button, Alert, CircularProgress, Backdrop } from '@mui/material'; diff --git a/Frontend/src/components/Modals/Usuarios/PerfilFormModal.tsx b/Frontend/src/components/Modals/Usuarios/PerfilFormModal.tsx new file mode 100644 index 0000000..5d458ae --- /dev/null +++ b/Frontend/src/components/Modals/Usuarios/PerfilFormModal.tsx @@ -0,0 +1,128 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; +import type { PerfilDto } from '../../../models/dtos/Usuarios/PerfilDto'; +import type { CreatePerfilDto } from '../../../models/dtos/Usuarios/CreatePerfilDto'; +import type { UpdatePerfilDto } from '../../../models/dtos/Usuarios/UpdatePerfilDto'; + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +interface PerfilFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreatePerfilDto | (UpdatePerfilDto & { id: number })) => Promise; + initialData?: PerfilDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const PerfilFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [nombrePerfil, setNombrePerfil] = useState(''); + const [descripcion, setDescripcion] = useState(''); + const [loading, setLoading] = useState(false); + const [localErrorNombre, setLocalErrorNombre] = useState(null); + + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + setNombrePerfil(initialData?.nombrePerfil || ''); + setDescripcion(initialData?.descripcion || ''); + setLocalErrorNombre(null); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const handleInputChange = () => { + if (localErrorNombre) setLocalErrorNombre(null); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setLocalErrorNombre(null); + clearErrorMessage(); + + if (!nombrePerfil.trim()) { + setLocalErrorNombre('El nombre del perfil es obligatorio.'); + return; + } + + setLoading(true); + try { + const dataToSubmit = { nombrePerfil, descripcion: descripcion || undefined }; + + if (isEditing && initialData) { + await onSubmit({ ...dataToSubmit, id: initialData.id }); + } else { + await onSubmit(dataToSubmit as CreatePerfilDto); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de PerfilFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Perfil' : 'Agregar Nuevo Perfil'} + + + { setNombrePerfil(e.target.value); handleInputChange(); }} + margin="normal" + error={!!localErrorNombre} + helperText={localErrorNombre || ''} + disabled={loading} + autoFocus + /> + setDescripcion(e.target.value)} + margin="normal" + multiline + rows={3} + disabled={loading} + /> + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default PerfilFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Usuarios/PermisoFormModal.tsx b/Frontend/src/components/Modals/Usuarios/PermisoFormModal.tsx new file mode 100644 index 0000000..ffbe18e --- /dev/null +++ b/Frontend/src/components/Modals/Usuarios/PermisoFormModal.tsx @@ -0,0 +1,135 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; +import type { PermisoDto } from '../../../models/dtos/Usuarios/PermisoDto'; // Usamos el DTO de la API para listar +import type { CreatePermisoDto } from '../../../models/dtos/Usuarios/CreatePermisoDto'; +import type { UpdatePermisoDto } from '../../../models/dtos/Usuarios/UpdatePermisoDto'; + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 450, // Un poco más ancho para los campos + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +interface PermisoFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreatePermisoDto | (UpdatePermisoDto & { id: number })) => Promise; + initialData?: PermisoDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const PermisoFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [modulo, setModulo] = useState(''); + const [descPermiso, setDescPermiso] = useState(''); + const [codAcc, setCodAcc] = useState(''); + const [loading, setLoading] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + setModulo(initialData?.modulo || ''); + setDescPermiso(initialData?.descPermiso || ''); + setCodAcc(initialData?.codAcc || ''); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!modulo.trim()) errors.modulo = 'El módulo es obligatorio.'; + if (!descPermiso.trim()) errors.descPermiso = 'La descripción es obligatoria.'; + if (!codAcc.trim()) errors.codAcc = 'El código de acceso es obligatorio.'; + else if (codAcc.length > 10) errors.codAcc = 'El código de acceso no debe exceder los 10 caracteres.'; + // Aquí podrías añadir validación de formato para codAcc si es necesario. + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (setter: React.Dispatch>, fieldName: string) => (e: React.ChangeEvent) => { + setter(e.target.value); + if (localErrors[fieldName]) { + setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + } + if (errorMessage) clearErrorMessage(); + }; + + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + const dataToSubmit = { modulo, descPermiso, codAcc }; + + if (isEditing && initialData) { + await onSubmit({ ...dataToSubmit, id: initialData.id }); + } else { + await onSubmit(dataToSubmit as CreatePermisoDto); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de PermisoFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Permiso' : 'Agregar Nuevo Permiso'} + + + + + + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default PermisoFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Usuarios/PermisosChecklist.tsx b/Frontend/src/components/Modals/Usuarios/PermisosChecklist.tsx new file mode 100644 index 0000000..bb55653 --- /dev/null +++ b/Frontend/src/components/Modals/Usuarios/PermisosChecklist.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { Box, Checkbox, FormControlLabel, FormGroup, Typography, Paper } from '@mui/material'; // Quitar Grid +import type { PermisoAsignadoDto } from '../../../models/dtos/Usuarios/PermisoAsignadoDto'; + +interface PermisosChecklistProps { + permisosDisponibles: PermisoAsignadoDto[]; + permisosSeleccionados: Set; + onPermisoChange: (permisoId: number, asignado: boolean) => void; + disabled?: boolean; +} + +const PermisosChecklist: React.FC = ({ + permisosDisponibles, + permisosSeleccionados, + onPermisoChange, + disabled = false, +}) => { + const permisosAgrupados = permisosDisponibles.reduce((acc, permiso) => { + const modulo = permiso.modulo || 'Otros'; + if (!acc[modulo]) { + acc[modulo] = []; + } + acc[modulo].push(permiso); + return acc; + }, {} as Record); + + return ( + {/* Contenedor Flexbox */} + {Object.entries(permisosAgrupados).map(([modulo, permisosDelModulo]) => ( + + + + {modulo} + + {/* Para que ocupe el espacio vertical */} + {permisosDelModulo.map((permiso) => ( + onPermisoChange(permiso.id, e.target.checked)} + disabled={disabled} + size="small" + /> + } + label={{`${permiso.descPermiso} (${permiso.codAcc})`}} + /> + ))} + + + + ))} + + ); +}; + +export default PermisosChecklist; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Usuarios/SetPasswordModal.tsx b/Frontend/src/components/Modals/Usuarios/SetPasswordModal.tsx new file mode 100644 index 0000000..bb59651 --- /dev/null +++ b/Frontend/src/components/Modals/Usuarios/SetPasswordModal.tsx @@ -0,0 +1,122 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControlLabel, Checkbox } from '@mui/material'; +import type { SetPasswordRequestDto } from '../../../models/dtos/Usuarios/SetPasswordRequestDto'; +import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto'; + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +interface SetPasswordModalProps { + open: boolean; + onClose: () => void; + onSubmit: (userId: number, data: SetPasswordRequestDto) => Promise; + usuario: UsuarioDto | null; // Usuario para el cual se setea la clave + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const SetPasswordModal: React.FC = ({ + open, + onClose, + onSubmit, + usuario, + errorMessage, + clearErrorMessage +}) => { + const [newPassword, setNewPassword] = useState(''); + const [confirmNewPassword, setConfirmNewPassword] = useState(''); + const [forceChange, setForceChange] = useState(true); + const [loading, setLoading] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + useEffect(() => { + if (open) { + setNewPassword(''); + setConfirmNewPassword(''); + setForceChange(true); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!newPassword) errors.newPassword = 'La nueva contraseña es obligatoria.'; + else if (newPassword.length < 6) errors.newPassword = 'La contraseña debe tener al menos 6 caracteres.'; + if (newPassword !== confirmNewPassword) errors.confirmNewPassword = 'Las contraseñas no coinciden.'; + if (usuario && usuario.user.toLowerCase() === newPassword.toLowerCase()) errors.newPassword = 'La contraseña no puede ser igual al nombre de usuario.' + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) { + setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + } + if (errorMessage) clearErrorMessage(); + }; + + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate() || !usuario) return; + + setLoading(true); + try { + const dataToSubmit: SetPasswordRequestDto = { newPassword, forceChangeOnNextLogin: forceChange }; + await onSubmit(usuario.id, dataToSubmit); + onClose(); + } catch (error) { + console.error("Error en submit de SetPasswordModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + Establecer Contraseña para {usuario?.user || ''} + + + {setNewPassword(e.target.value); handleInputChange('newPassword');}} margin="dense" + error={!!localErrors.newPassword} helperText={localErrors.newPassword || ''} + disabled={loading} autoFocus + /> + {setConfirmNewPassword(e.target.value); handleInputChange('confirmNewPassword');}} margin="dense" + error={!!localErrors.confirmNewPassword} helperText={localErrors.confirmNewPassword || ''} + disabled={loading} + /> + setForceChange(e.target.checked)} disabled={loading}/>} + label="Forzar cambio en próximo inicio de sesión" + sx={{ mt: 1 }} + /> + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default SetPasswordModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Usuarios/UsuarioFormModal.tsx b/Frontend/src/components/Modals/Usuarios/UsuarioFormModal.tsx new file mode 100644 index 0000000..a42340d --- /dev/null +++ b/Frontend/src/components/Modals/Usuarios/UsuarioFormModal.tsx @@ -0,0 +1,255 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox +} from '@mui/material'; +import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto'; +import type { CreateUsuarioRequestDto } from '../../../models/dtos/Usuarios/CreateUsuarioRequestDto'; +import type { UpdateUsuarioRequestDto } from '../../../models/dtos/Usuarios/UpdateUsuarioRequestDto'; +import type { PerfilDto } from '../../../models/dtos/Usuarios/PerfilDto'; // Para el dropdown de perfiles +import perfilService from '../../../services/Usuarios/perfilService'; // Para obtener la lista de perfiles + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 500 }, // Responsive width + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', // Para permitir scroll si el contenido es mucho + overflowY: 'auto' // Habilitar scroll vertical +}; + +interface UsuarioFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateUsuarioRequestDto | UpdateUsuarioRequestDto, id?: number) => Promise; + initialData?: UsuarioDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const UsuarioFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [user, setUser] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [nombre, setNombre] = useState(''); + const [apellido, setApellido] = useState(''); + const [idPerfil, setIdPerfil] = useState(''); // Puede ser string vacío inicialmente + const [habilitada, setHabilitada] = useState(true); + const [supAdmin, setSupAdmin] = useState(false); + const [debeCambiarClave, setDebeCambiarClave] = useState(true); + const [verLog, setVerLog] = useState('1.0.0.0'); + + const [perfiles, setPerfiles] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingPerfiles, setLoadingPerfiles] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchPerfiles = async () => { + setLoadingPerfiles(true); + try { + const data = await perfilService.getAllPerfiles(); + setPerfiles(data); + } catch (error) { + console.error("Error al cargar perfiles", error); + setLocalErrors(prev => ({...prev, perfiles: 'Error al cargar perfiles.'})); + } finally { + setLoadingPerfiles(false); + } + }; + + if (open) { + fetchPerfiles(); + setUser(initialData?.user || ''); + // No pre-rellenar contraseña en edición + setPassword(''); + setConfirmPassword(''); + setNombre(initialData?.nombre || ''); + setApellido(initialData?.apellido || ''); + setIdPerfil(initialData?.idPerfil || ''); + setHabilitada(initialData ? initialData.habilitada : true); + setSupAdmin(initialData ? initialData.supAdmin : false); + setDebeCambiarClave(initialData ? initialData.debeCambiarClave : !isEditing); // true para creación + setVerLog(initialData?.verLog || '1.0.0.0'); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage, isEditing]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!user.trim()) errors.user = 'El nombre de usuario es obligatorio.'; + else if (user.length < 3) errors.user = 'El usuario debe tener al menos 3 caracteres.'; + + if (!isEditing || (isEditing && password)) { // Validar contraseña solo si se está creando o si se ingresó algo en edición + if (!password) errors.password = 'La contraseña es obligatoria.'; + else if (password.length < 6) errors.password = 'La contraseña debe tener al menos 6 caracteres.'; + if (password !== confirmPassword) errors.confirmPassword = 'Las contraseñas no coinciden.'; + if (user.trim().toLowerCase() === password.toLowerCase()) errors.password = 'La contraseña no puede ser igual al nombre de usuario.' + } + + if (!nombre.trim()) errors.nombre = 'El nombre es obligatorio.'; + if (!apellido.trim()) errors.apellido = 'El apellido es obligatorio.'; + if (!idPerfil) errors.idPerfil = 'Debe seleccionar un perfil.'; + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) { + setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + } + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + if (isEditing && initialData) { + const dataToSubmit: UpdateUsuarioRequestDto = { + nombre, apellido, idPerfil: Number(idPerfil), habilitada, supAdmin, debeCambiarClave, verLog + }; + await onSubmit(dataToSubmit, initialData.id); + // Si se ingresó una nueva contraseña, llamar a un endpoint separado para cambiarla + if (password) { + // Esto requeriría un endpoint adicional en el backend para que un admin cambie la clave de otro user + // o adaptar el flujo de `AuthService.ChangePasswordAsync` si es posible, + // o un nuevo endpoint en UsuarioController para setear clave. + // Por ahora, lo dejamos así, la clave se cambia por el propio usuario o por un reset del admin. + console.warn("El cambio de contraseña en edición de usuario no está implementado en este modal directamente. Usar la opción de 'Resetear Contraseña'."); + } + + } else { + const dataToSubmit: CreateUsuarioRequestDto = { + user, password, nombre, apellido, idPerfil: Number(idPerfil), habilitada, supAdmin, debeCambiarClave, verLog + }; + await onSubmit(dataToSubmit); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de UsuarioFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Usuario' : 'Agregar Nuevo Usuario'} + + + {/* SECCIÓN DE CAMPOS CON BOX Y FLEXBOX */} + {/* Contenedor principal de campos */} + {/* Fila 1 */} + {setUser(e.target.value); handleInputChange('user');}} margin="dense" + error={!!localErrors.user} helperText={localErrors.user || ''} + disabled={loading || isEditing} autoFocus={!isEditing} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} // 8px es la mitad del gap + /> + + Perfil + + {localErrors.idPerfil && {localErrors.idPerfil}} + {loadingPerfiles && } + + + + {!isEditing && ( + {/* Fila 2 (Contraseñas) */} + {setPassword(e.target.value); handleInputChange('password');}} margin="dense" + error={!!localErrors.password} helperText={localErrors.password || ''} + disabled={loading} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} + /> + {setConfirmPassword(e.target.value); handleInputChange('confirmPassword');}} margin="dense" + error={!!localErrors.confirmPassword} helperText={localErrors.confirmPassword || ''} + disabled={loading} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} + /> + + )} + + {/* Fila 3 (Nombre y Apellido) */} + {setNombre(e.target.value); handleInputChange('nombre');}} margin="dense" + error={!!localErrors.nombre} helperText={localErrors.nombre || ''} + disabled={loading} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} + /> + {setApellido(e.target.value); handleInputChange('apellido');}} margin="dense" + error={!!localErrors.apellido} helperText={localErrors.apellido || ''} + disabled={loading} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} + /> + + + {/* Fila 4 (VerLog y Habilitado) */} + setVerLog(e.target.value)} margin="dense" + disabled={loading} + sx={{ flex: 1, minWidth: 'calc(50% - 8px)' }} + /> + + setHabilitada(e.target.checked)} disabled={loading}/>} label="Habilitado" /> + + + + {/* Fila 5 (Checkboxes) */} + + setSupAdmin(e.target.checked)} disabled={loading}/>} label="Super Administrador" /> + + + setDebeCambiarClave(e.target.checked)} disabled={loading}/>} label="Debe Cambiar Clave" /> + + + {/* Fin contenedor principal de campos */} + + + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default UsuarioFormModal; \ No newline at end of file diff --git a/Frontend/src/contexts/AuthContext.tsx b/Frontend/src/contexts/AuthContext.tsx index 697a902..bb5b7be 100644 --- a/Frontend/src/contexts/AuthContext.tsx +++ b/Frontend/src/contexts/AuthContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useState, useContext, type ReactNode, useEffect } from 'react'; -import type { LoginResponseDto } from '../models/dtos/LoginResponseDto'; +import type { LoginResponseDto } from '../models/dtos/Usuarios/LoginResponseDto'; import { jwtDecode } from 'jwt-decode'; // Interfaz para los datos del usuario que guardaremos en el contexto diff --git a/Frontend/src/layouts/MainLayout.tsx b/Frontend/src/layouts/MainLayout.tsx index 1e36878..86debf1 100644 --- a/Frontend/src/layouts/MainLayout.tsx +++ b/Frontend/src/layouts/MainLayout.tsx @@ -1,7 +1,7 @@ import React, { type ReactNode, useState, useEffect } from 'react'; import { Box, AppBar, Toolbar, Typography, Button, Tabs, Tab, Paper } from '@mui/material'; import { useAuth } from '../contexts/AuthContext'; -import ChangePasswordModal from '../components/ChangePasswordModal'; +import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal'; import { useNavigate, useLocation } from 'react-router-dom'; // Para manejar la navegación y la ruta actual interface MainLayoutProps { diff --git a/Frontend/src/models/dtos/Distribucion/CanillaDto.ts b/Frontend/src/models/dtos/Distribucion/CanillaDto.ts new file mode 100644 index 0000000..8ad4ebf --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CanillaDto.ts @@ -0,0 +1,14 @@ +export interface CanillaDto { + idCanilla: number; + legajo?: number | null; + nomApe: string; + parada?: string | null; + idZona: number; + nombreZona: string; + accionista: boolean; + obs?: string | null; + empresa: number; + nombreEmpresa: string; + baja: boolean; + fechaBaja?: string | null; // string dd/MM/yyyy +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreateCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/CreateCanillaDto.ts new file mode 100644 index 0000000..70649b7 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreateCanillaDto.ts @@ -0,0 +1,9 @@ +export interface CreateCanillaDto { + legajo?: number | null; + nomApe: string; + parada?: string | null; + idZona: number; + accionista: boolean; + obs?: string | null; + empresa: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreateDistribuidorDto.ts b/Frontend/src/models/dtos/Distribucion/CreateDistribuidorDto.ts new file mode 100644 index 0000000..4b69049 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreateDistribuidorDto.ts @@ -0,0 +1,13 @@ +export interface CreateDistribuidorDto { + nombre: string; + contacto?: string | null; + nroDoc: string; + idZona?: number | null; + calle?: string | null; + numero?: string | null; + piso?: string | null; + depto?: string | null; + telefono?: string | null; + email?: string | null; + localidad?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Empresas/CreateEmpresaDto.ts b/Frontend/src/models/dtos/Distribucion/CreateEmpresaDto.ts similarity index 100% rename from Frontend/src/models/dtos/Empresas/CreateEmpresaDto.ts rename to Frontend/src/models/dtos/Distribucion/CreateEmpresaDto.ts diff --git a/Frontend/src/models/dtos/Distribucion/CreateOtroDestinoDto.ts b/Frontend/src/models/dtos/Distribucion/CreateOtroDestinoDto.ts new file mode 100644 index 0000000..c438df8 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreateOtroDestinoDto.ts @@ -0,0 +1,4 @@ +export interface CreateOtroDestinoDto { + nombre: string; + obs?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreatePrecioDto.ts b/Frontend/src/models/dtos/Distribucion/CreatePrecioDto.ts new file mode 100644 index 0000000..ec728b4 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreatePrecioDto.ts @@ -0,0 +1,12 @@ +export interface CreatePrecioDto { + idPublicacion: number; // Importante para la ruta y para el backend + vigenciaD: string; // "yyyy-MM-dd" + // VigenciaH no se envía al crear, se calcula en backend o se deja null + lunes?: number | null; + martes?: number | null; + miercoles?: number | null; + jueves?: number | null; + viernes?: number | null; + sabado?: number | null; + domingo?: number | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreatePublicacionDto.ts b/Frontend/src/models/dtos/Distribucion/CreatePublicacionDto.ts new file mode 100644 index 0000000..fda3f15 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreatePublicacionDto.ts @@ -0,0 +1,7 @@ +export interface CreatePublicacionDto { + nombre: string; + observacion?: string | null; + idEmpresa: number; + ctrlDevoluciones: boolean; + habilitada: boolean; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/DistribuidorDto.ts b/Frontend/src/models/dtos/Distribucion/DistribuidorDto.ts new file mode 100644 index 0000000..34e29e8 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/DistribuidorDto.ts @@ -0,0 +1,15 @@ +export interface DistribuidorDto { + idDistribuidor: number; + nombre: string; + contacto?: string | null; + nroDoc: string; + idZona?: number | null; + nombreZona?: string | null; + calle?: string | null; + numero?: string | null; + piso?: string | null; + depto?: string | null; + telefono?: string | null; + email?: string | null; + localidad?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Empresas/EmpresaDto.ts b/Frontend/src/models/dtos/Distribucion/EmpresaDto.ts similarity index 100% rename from Frontend/src/models/dtos/Empresas/EmpresaDto.ts rename to Frontend/src/models/dtos/Distribucion/EmpresaDto.ts diff --git a/Frontend/src/models/dtos/Distribucion/OtroDestinoDto.ts b/Frontend/src/models/dtos/Distribucion/OtroDestinoDto.ts new file mode 100644 index 0000000..6c0516e --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/OtroDestinoDto.ts @@ -0,0 +1,5 @@ +export interface OtroDestinoDto { + idDestino: number; + nombre: string; + obs?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/PrecioDto.ts b/Frontend/src/models/dtos/Distribucion/PrecioDto.ts new file mode 100644 index 0000000..915789f --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/PrecioDto.ts @@ -0,0 +1,13 @@ +export interface PrecioDto { + idPrecio: number; + idPublicacion: number; + vigenciaD: string; // "yyyy-MM-dd" + vigenciaH?: string | null; // "yyyy-MM-dd" + lunes?: number | null; + martes?: number | null; + miercoles?: number | null; + jueves?: number | null; + viernes?: number | null; + sabado?: number | null; + domingo?: number | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/PublicacionDto.ts b/Frontend/src/models/dtos/Distribucion/PublicacionDto.ts new file mode 100644 index 0000000..598cf6f --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/PublicacionDto.ts @@ -0,0 +1,9 @@ +export interface PublicacionDto { + idPublicacion: number; + nombre: string; + observacion?: string | null; + idEmpresa: number; + nombreEmpresa: string; + ctrlDevoluciones: boolean; + habilitada: boolean; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/ToggleBajaCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/ToggleBajaCanillaDto.ts new file mode 100644 index 0000000..606edec --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/ToggleBajaCanillaDto.ts @@ -0,0 +1,3 @@ +export interface ToggleBajaCanillaDto { + darDeBaja: boolean; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdateCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/UpdateCanillaDto.ts new file mode 100644 index 0000000..6fbe0e9 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdateCanillaDto.ts @@ -0,0 +1,9 @@ +export interface UpdateCanillaDto { + legajo?: number | null; + nomApe: string; + parada?: string | null; + idZona: number; + accionista: boolean; + obs?: string | null; + empresa: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdateDistribuidorDto.ts b/Frontend/src/models/dtos/Distribucion/UpdateDistribuidorDto.ts new file mode 100644 index 0000000..d38e701 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdateDistribuidorDto.ts @@ -0,0 +1,13 @@ +export interface UpdateDistribuidorDto { + nombre: string; + contacto?: string | null; + nroDoc: string; + idZona?: number | null; + calle?: string | null; + numero?: string | null; + piso?: string | null; + depto?: string | null; + telefono?: string | null; + email?: string | null; + localidad?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Empresas/UpdateEmpresaDto.ts b/Frontend/src/models/dtos/Distribucion/UpdateEmpresaDto.ts similarity index 100% rename from Frontend/src/models/dtos/Empresas/UpdateEmpresaDto.ts rename to Frontend/src/models/dtos/Distribucion/UpdateEmpresaDto.ts diff --git a/Frontend/src/models/dtos/Distribucion/UpdateOtroDestinoDto.ts b/Frontend/src/models/dtos/Distribucion/UpdateOtroDestinoDto.ts new file mode 100644 index 0000000..f5fd251 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdateOtroDestinoDto.ts @@ -0,0 +1,4 @@ +export interface UpdateOtroDestinoDto { + nombre: string; + obs?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdatePrecioDto.ts b/Frontend/src/models/dtos/Distribucion/UpdatePrecioDto.ts new file mode 100644 index 0000000..6cd11e6 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdatePrecioDto.ts @@ -0,0 +1,11 @@ +// Para actualizar, principalmente se modifican los montos o se cierra un periodo con VigenciaH +export interface UpdatePrecioDto { + vigenciaH?: string | null; // "yyyy-MM-dd", para cerrar un periodo + lunes?: number | null; + martes?: number | null; + miercoles?: number | null; + jueves?: number | null; + viernes?: number | null; + sabado?: number | null; + domingo?: number | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdatePublicacionDto.ts b/Frontend/src/models/dtos/Distribucion/UpdatePublicacionDto.ts new file mode 100644 index 0000000..60c537e --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdatePublicacionDto.ts @@ -0,0 +1,7 @@ +export interface UpdatePublicacionDto { + nombre: string; + observacion?: string | null; + idEmpresa: number; + ctrlDevoluciones: boolean; + habilitada: boolean; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/LoginRequestDto.ts b/Frontend/src/models/dtos/LoginRequestDto.ts deleted file mode 100644 index b6b31ce..0000000 --- a/Frontend/src/models/dtos/LoginRequestDto.ts +++ /dev/null @@ -1,5 +0,0 @@ -// src/models/dtos/LoginRequestDto.ts -export interface LoginRequestDto { - Username: string; // Coincide con las propiedades C# - Password: string; -} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/ActualizarPermisosPerfilRequestDto.ts b/Frontend/src/models/dtos/Usuarios/ActualizarPermisosPerfilRequestDto.ts new file mode 100644 index 0000000..5e0da36 --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/ActualizarPermisosPerfilRequestDto.ts @@ -0,0 +1,3 @@ +export interface ActualizarPermisosPerfilRequestDto { + permisosIds: number[]; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/ChangePasswordRequestDto.ts b/Frontend/src/models/dtos/Usuarios/ChangePasswordRequestDto.ts similarity index 72% rename from Frontend/src/models/dtos/ChangePasswordRequestDto.ts rename to Frontend/src/models/dtos/Usuarios/ChangePasswordRequestDto.ts index 652113f..c6bbb61 100644 --- a/Frontend/src/models/dtos/ChangePasswordRequestDto.ts +++ b/Frontend/src/models/dtos/Usuarios/ChangePasswordRequestDto.ts @@ -1,4 +1,3 @@ -// src/models/dtos/ChangePasswordRequestDto.ts export interface ChangePasswordRequestDto { currentPassword: string; newPassword: string; diff --git a/Frontend/src/models/dtos/Usuarios/CreatePerfilDto.ts b/Frontend/src/models/dtos/Usuarios/CreatePerfilDto.ts new file mode 100644 index 0000000..ad6a39e --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/CreatePerfilDto.ts @@ -0,0 +1,4 @@ +export interface CreatePerfilDto { + nombrePerfil: string; + descripcion?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/CreatePermisoDto.ts b/Frontend/src/models/dtos/Usuarios/CreatePermisoDto.ts new file mode 100644 index 0000000..ceb5888 --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/CreatePermisoDto.ts @@ -0,0 +1,5 @@ +export interface CreatePermisoDto { + modulo: string; + descPermiso: string; + codAcc: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/CreateUsuarioRequestDto.ts b/Frontend/src/models/dtos/Usuarios/CreateUsuarioRequestDto.ts new file mode 100644 index 0000000..80ed237 --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/CreateUsuarioRequestDto.ts @@ -0,0 +1,11 @@ +export interface CreateUsuarioRequestDto { + user: string; + password?: string; // Puede ser opcional si la clave se genera o se fuerza cambio + nombre: string; + apellido: string; + idPerfil: number; + habilitada?: boolean; + supAdmin?: boolean; + debeCambiarClave?: boolean; + verLog?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/LoginRequestDto.ts b/Frontend/src/models/dtos/Usuarios/LoginRequestDto.ts new file mode 100644 index 0000000..5000190 --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/LoginRequestDto.ts @@ -0,0 +1,4 @@ +export interface LoginRequestDto { + Username: string; + Password: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/LoginResponseDto.ts b/Frontend/src/models/dtos/Usuarios/LoginResponseDto.ts similarity index 64% rename from Frontend/src/models/dtos/LoginResponseDto.ts rename to Frontend/src/models/dtos/Usuarios/LoginResponseDto.ts index dbfd953..d1deea7 100644 --- a/Frontend/src/models/dtos/LoginResponseDto.ts +++ b/Frontend/src/models/dtos/Usuarios/LoginResponseDto.ts @@ -1,4 +1,3 @@ -// src/models/dtos/LoginResponseDto.ts export interface LoginResponseDto { token: string; userId: number; @@ -6,5 +5,4 @@ export interface LoginResponseDto { nombreCompleto: string; esSuperAdmin: boolean; debeCambiarClave: boolean; - // Añade otros campos si los definiste en el DTO C# } \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/PerfilDto.ts b/Frontend/src/models/dtos/Usuarios/PerfilDto.ts new file mode 100644 index 0000000..29e02cc --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/PerfilDto.ts @@ -0,0 +1,5 @@ +export interface PerfilDto { + id: number; + nombrePerfil: string; + descripcion?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/PermisoAsignadoDto.ts b/Frontend/src/models/dtos/Usuarios/PermisoAsignadoDto.ts new file mode 100644 index 0000000..a952a62 --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/PermisoAsignadoDto.ts @@ -0,0 +1,7 @@ +export interface PermisoAsignadoDto { + id: number; + modulo: string; + descPermiso: string; + codAcc: string; + asignado: boolean; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/PermisoDto.ts b/Frontend/src/models/dtos/Usuarios/PermisoDto.ts new file mode 100644 index 0000000..98a3570 --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/PermisoDto.ts @@ -0,0 +1,7 @@ +export interface PermisoDto { + id: number; + modulo: string; + descPermiso: string; + codAcc: string; + asignado?: boolean; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/SetPasswordRequestDto.ts b/Frontend/src/models/dtos/Usuarios/SetPasswordRequestDto.ts new file mode 100644 index 0000000..6ccb9b2 --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/SetPasswordRequestDto.ts @@ -0,0 +1,4 @@ +export interface SetPasswordRequestDto { + newPassword: string; + forceChangeOnNextLogin?: boolean; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/UpdatePerfilDto.ts b/Frontend/src/models/dtos/Usuarios/UpdatePerfilDto.ts new file mode 100644 index 0000000..e7c988d --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/UpdatePerfilDto.ts @@ -0,0 +1,4 @@ +export interface UpdatePerfilDto { + nombrePerfil: string; + descripcion?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/UpdatePermisoDto.ts b/Frontend/src/models/dtos/Usuarios/UpdatePermisoDto.ts new file mode 100644 index 0000000..bfc4c7f --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/UpdatePermisoDto.ts @@ -0,0 +1,5 @@ +export interface UpdatePermisoDto { + modulo: string; + descPermiso: string; + codAcc: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/UpdateUsuarioRequestDto.ts b/Frontend/src/models/dtos/Usuarios/UpdateUsuarioRequestDto.ts new file mode 100644 index 0000000..9456de4 --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/UpdateUsuarioRequestDto.ts @@ -0,0 +1,9 @@ +export interface UpdateUsuarioRequestDto { + nombre: string; + apellido: string; + idPerfil: number; + habilitada: boolean; + supAdmin: boolean; + debeCambiarClave: boolean; + verLog?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/UsuarioDto.ts b/Frontend/src/models/dtos/Usuarios/UsuarioDto.ts new file mode 100644 index 0000000..f1eab92 --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/UsuarioDto.ts @@ -0,0 +1,12 @@ +export interface UsuarioDto { + id: number; + user: string; + habilitada: boolean; + supAdmin: boolean; + nombre: string; + apellido: string; + idPerfil: number; + nombrePerfil: string; + debeCambiarClave: boolean; + verLog: string; +} \ No newline at end of file diff --git a/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx b/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx index ddaa0d5..5b973cf 100644 --- a/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx +++ b/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx @@ -7,11 +7,11 @@ import { } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import MoreVertIcon from '@mui/icons-material/MoreVert'; -import tipoPagoService from '../../services/tipoPagoService'; +import tipoPagoService from '../../services/Contables/tipoPagoService'; import type { TipoPago } from '../../models/Entities/TipoPago'; import type { CreateTipoPagoDto } from '../../models/dtos/tiposPago/CreateTipoPagoDto'; import type { UpdateTipoPagoDto } from '../../models/dtos/tiposPago/UpdateTipoPagoDto'; -import TipoPagoFormModal from '../../components/Modals/TipoPagoFormModal'; +import TipoPagoFormModal from '../../components/Modals/Contables/TipoPagoFormModal'; import axios from 'axios'; import { usePermissions } from '../../hooks/usePermissions'; diff --git a/Frontend/src/pages/Distribucion/CanillasPage.tsx b/Frontend/src/pages/Distribucion/CanillasPage.tsx deleted file mode 100644 index 16e3daf..0000000 --- a/Frontend/src/pages/Distribucion/CanillasPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; - -const CanillasPage: React.FC = () => { - return Página de Gestión de Canillas; -}; -export default CanillasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/DistribuidoresPage.tsx b/Frontend/src/pages/Distribucion/DistribuidoresPage.tsx deleted file mode 100644 index 706d12f..0000000 --- a/Frontend/src/pages/Distribucion/DistribuidoresPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; - -const DistribuidoresPage: React.FC = () => { - return Página de Gestión de Distribuidores; -}; -export default DistribuidoresPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarCanillitasPage.tsx b/Frontend/src/pages/Distribucion/GestionarCanillitasPage.tsx new file mode 100644 index 0000000..3f9a65f --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarCanillitasPage.tsx @@ -0,0 +1,225 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, Chip, FormControlLabel +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import ToggleOnIcon from '@mui/icons-material/ToggleOn'; +import ToggleOffIcon from '@mui/icons-material/ToggleOff'; +import canillaService from '../../services/Distribucion/canillaService'; +import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; +import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto'; +import type { UpdateCanillaDto } from '../../models/dtos/Distribucion/UpdateCanillaDto'; +import CanillaFormModal from '../../components/Modals/Distribucion/CanillaFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarCanillitasPage: React.FC = () => { + const [canillitas, setCanillitas] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroNomApe, setFiltroNomApe] = useState(''); + const [filtroLegajo, setFiltroLegajo] = useState(''); + const [filtroSoloActivos, setFiltroSoloActivos] = useState(true); + + + const [modalOpen, setModalOpen] = useState(false); + const [editingCanillita, setEditingCanillita] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedCanillitaRow, setSelectedCanillitaRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + + const puedeVer = isSuperAdmin || tienePermiso("CG001"); + const puedeCrear = isSuperAdmin || tienePermiso("CG002"); + const puedeModificar = isSuperAdmin || tienePermiso("CG003"); + // CG004 para Porcentajes/Montos, se gestionará por separado. + const puedeDarBaja = isSuperAdmin || tienePermiso("CG005"); + + const cargarCanillitas = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); + setLoading(false); + return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const legajoNum = filtroLegajo ? parseInt(filtroLegajo, 10) : undefined; + if (filtroLegajo && isNaN(legajoNum!)) { + setApiErrorMessage("Legajo debe ser un número."); + setCanillitas([]); // Limpiar resultados si el filtro es inválido + setLoading(false); + return; + } + const data = await canillaService.getAllCanillas(filtroNomApe, legajoNum, filtroSoloActivos); + setCanillitas(data); + } catch (err) { + console.error(err); setError('Error al cargar los canillitas.'); + } finally { setLoading(false); } + }, [filtroNomApe, filtroLegajo, filtroSoloActivos, puedeVer]); + + useEffect(() => { cargarCanillitas(); }, [cargarCanillitas]); + + const handleOpenModal = (canillita?: CanillaDto) => { + setEditingCanillita(canillita || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingCanillita(null); + }; + + const handleSubmitModal = async (data: CreateCanillaDto | UpdateCanillaDto, id?: number) => { + setApiErrorMessage(null); + try { + if (id && editingCanillita) { + await canillaService.updateCanilla(id, data as UpdateCanillaDto); + } else { + await canillaService.createCanilla(data as CreateCanillaDto); + } + cargarCanillitas(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el canillita.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleToggleBaja = async (canillita: CanillaDto) => { + setApiErrorMessage(null); + const accion = canillita.baja ? "reactivar" : "dar de baja"; + if (window.confirm(`¿Está seguro de que desea ${accion} a ${canillita.nomApe}?`)) { + try { + await canillaService.toggleBajaCanilla(canillita.idCanilla, { darDeBaja: !canillita.baja }); + cargarCanillitas(); + } catch (err:any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el canillita.`; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, canillita: CanillaDto) => { + setAnchorEl(event.currentTarget); setSelectedCanillitaRow(canillita); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedCanillitaRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = canillitas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeVer) { + return {error || "No tiene permiso."}; + } + + return ( + + Gestionar Canillitas + + + setFiltroNomApe(e.target.value)} + sx={{ flex: 2, minWidth: '250px' }} // Dar más espacio al nombre + /> + setFiltroLegajo(e.target.value)} + sx={{ flex: 1, minWidth: '150px' }} + /> + setFiltroSoloActivos(e.target.checked)} + size="small" + /> + } + label="Solo Activos" + sx={{ flexShrink: 0 }} // Para que el label no se comprima demasiado + /> + {/* */} + + {puedeCrear && ( + + + + )} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + + + LegajoNombre y Apellido + ZonaEmpresa + AccionistaEstado + Acciones + + + {displayData.length === 0 ? ( + No se encontraron canillitas. + ) : ( + displayData.map((c) => ( + + {c.legajo || '-'}{c.nomApe} + {c.nombreZona}{c.empresa === 0 ? '-' : c.nombreEmpresa} + {c.accionista ? : } + {c.baja ? : } + + handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeDarBaja}> + + + + + )))} + +
+ +
+ )} + + + {puedeModificar && ( { handleOpenModal(selectedCanillitaRow!); handleMenuClose(); }}>Modificar)} + {puedeDarBaja && selectedCanillitaRow && ( + handleToggleBaja(selectedCanillitaRow)}> + {selectedCanillitaRow.baja ? : } + {selectedCanillitaRow.baja ? 'Reactivar' : 'Dar de Baja'} + + )} + {(!puedeModificar && !puedeDarBaja) && Sin acciones} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarCanillitasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarDistribuidoresPage.tsx b/Frontend/src/pages/Distribucion/GestionarDistribuidoresPage.tsx new file mode 100644 index 0000000..05a387a --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarDistribuidoresPage.tsx @@ -0,0 +1,196 @@ +// src/pages/Distribucion/GestionarDistribuidoresPage.tsx +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import distribuidorService from '../../services/Distribucion/distribuidorService'; +import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; +import type { CreateDistribuidorDto } from '../../models/dtos/Distribucion/CreateDistribuidorDto'; +import type { UpdateDistribuidorDto } from '../../models/dtos/Distribucion/UpdateDistribuidorDto'; +import DistribuidorFormModal from '../../components/Modals/Distribucion/DistribuidorFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + + +const GestionarDistribuidoresPage: React.FC = () => { + const [distribuidores, setDistribuidores] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroNombre, setFiltroNombre] = useState(''); + const [filtroNroDoc, setFiltroNroDoc] = useState(''); + + const [modalOpen, setModalOpen] = useState(false); + const [editingDistribuidor, setEditingDistribuidor] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedDistribuidorRow, setSelectedDistribuidorRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + + const puedeVer = isSuperAdmin || tienePermiso("DG001"); + const puedeCrear = isSuperAdmin || tienePermiso("DG002"); + const puedeModificar = isSuperAdmin || tienePermiso("DG003"); + const puedeEliminar = isSuperAdmin || tienePermiso("DG005"); + + const cargarDistribuidores = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); + setLoading(false); + return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const data = await distribuidorService.getAllDistribuidores(filtroNombre, filtroNroDoc); + setDistribuidores(data); + } catch (err) { + console.error(err); setError('Error al cargar los distribuidores.'); + } finally { setLoading(false); } + }, [filtroNombre, filtroNroDoc, puedeVer]); + + useEffect(() => { cargarDistribuidores(); }, [cargarDistribuidores]); + + const handleOpenModal = (distribuidor?: DistribuidorDto) => { + setEditingDistribuidor(distribuidor || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingDistribuidor(null); + }; + + const handleSubmitModal = async (data: CreateDistribuidorDto | UpdateDistribuidorDto, id?: number) => { + setApiErrorMessage(null); + try { + if (id && editingDistribuidor) { + await distribuidorService.updateDistribuidor(id, data as UpdateDistribuidorDto); + } else { + await distribuidorService.createDistribuidor(data as CreateDistribuidorDto); + } + cargarDistribuidores(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el distribuidor.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm(`¿Está seguro de eliminar este distribuidor (ID: ${id})?`)) { + setApiErrorMessage(null); + try { + await distribuidorService.deleteDistribuidor(id); + cargarDistribuidores(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el distribuidor.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, distribuidor: DistribuidorDto) => { + setAnchorEl(event.currentTarget); setSelectedDistribuidorRow(distribuidor); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedDistribuidorRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = distribuidores.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeVer) { + return {error || "No tiene permiso."}; + } + + return ( + + Gestionar Distribuidores + + + setFiltroNombre(e.target.value)} + sx={{ flexGrow: 1, minWidth: '200px' }} + /> + setFiltroNroDoc(e.target.value)} + sx={{ flexGrow: 1, minWidth: '200px' }} + /> + {/* */} + + {puedeCrear && ( + + + + )} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + + + NombreNro. Doc. + ContactoZona + TeléfonoLocalidad + {(puedeModificar || puedeEliminar) && Acciones} + + + {displayData.length === 0 ? ( + No se encontraron distribuidores. + ) : ( + displayData.map((d) => ( + + {d.nombre}{d.nroDoc} + {d.contacto || '-'}{d.nombreZona || '-'} + {d.telefono || '-'}{d.localidad || '-'} + {(puedeModificar || puedeEliminar) && ( + + handleMenuOpen(e, d)} disabled={!puedeModificar && !puedeEliminar}> + + )} + + )))} + +
+ +
+ )} + + + {puedeModificar && ( { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}>Modificar)} + {puedeEliminar && ( handleDelete(selectedDistribuidorRow!.idDistribuidor)}>Eliminar)} + {(!puedeModificar && !puedeEliminar) && Sin acciones} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarDistribuidoresPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarEmpresasPage.tsx b/Frontend/src/pages/Distribucion/GestionarEmpresasPage.tsx index 3457670..feb1396 100644 --- a/Frontend/src/pages/Distribucion/GestionarEmpresasPage.tsx +++ b/Frontend/src/pages/Distribucion/GestionarEmpresasPage.tsx @@ -6,11 +6,11 @@ import { } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import MoreVertIcon from '@mui/icons-material/MoreVert'; -import empresaService from '../../services/empresaService'; // Importar el servicio de Empresas -import type { EmpresaDto } from '../../models/dtos/Empresas/EmpresaDto'; -import type { CreateEmpresaDto } from '../../models/dtos/Empresas/CreateEmpresaDto'; -import type { UpdateEmpresaDto } from '../../models/dtos/Empresas/UpdateEmpresaDto'; -import EmpresaFormModal from '../../components/Modals/EmpresaFormModal'; // Importar el modal de Empresas +import empresaService from '../../services/Distribucion/empresaService'; // Importar el servicio de Empresas +import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; +import type { CreateEmpresaDto } from '../../models/dtos/Distribucion/CreateEmpresaDto'; +import type { UpdateEmpresaDto } from '../../models/dtos/Distribucion/UpdateEmpresaDto'; +import EmpresaFormModal from '../../components/Modals/Distribucion/EmpresaFormModal'; // Importar el modal de Empresas import { usePermissions } from '../../hooks/usePermissions'; // Importar hook de permisos import axios from 'axios'; // Para manejo de errores de API @@ -163,7 +163,6 @@ const GestionarEmpresasPage: React.FC = () => { size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} - // Puedes añadir un botón de buscar explícito o dejar que filtre al escribir /> {/* Mostrar botón de agregar solo si tiene permiso */} diff --git a/Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx b/Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx new file mode 100644 index 0000000..a9244f5 --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarOtrosDestinosPage.tsx @@ -0,0 +1,228 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import otroDestinoService from '../../services/Distribucion/otroDestinoService'; +import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto'; +import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto'; +import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto'; +import OtroDestinoFormModal from '../../components/Modals/Distribucion/OtroDestinoFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarOtrosDestinosPage: React.FC = () => { + const [otrosDestinos, setOtrosDestinos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroNombre, setFiltroNombre] = useState(''); + + const [modalOpen, setModalOpen] = useState(false); + const [editingDestino, setEditingDestino] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedDestinoRow, setSelectedDestinoRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + + // Permisos para Otros Destinos (OD001 a OD004) - Revisa tus códigos de permiso + const puedeVer = isSuperAdmin || tienePermiso("OD001"); // Asumiendo OD001 es ver entidad + const puedeCrear = isSuperAdmin || tienePermiso("OD002"); + const puedeModificar = isSuperAdmin || tienePermiso("OD003"); + const puedeEliminar = isSuperAdmin || tienePermiso("OD004"); + + const cargarOtrosDestinos = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); + setLoading(false); + return; + } + setLoading(true); + setError(null); + setApiErrorMessage(null); + try { + const data = await otroDestinoService.getAllOtrosDestinos(filtroNombre); + setOtrosDestinos(data); + } catch (err) { + console.error(err); + setError('Error al cargar los otros destinos.'); + } finally { + setLoading(false); + } + }, [filtroNombre, puedeVer]); + + useEffect(() => { + cargarOtrosDestinos(); + }, [cargarOtrosDestinos]); + + const handleOpenModal = (destino?: OtroDestinoDto) => { + setEditingDestino(destino || null); + setApiErrorMessage(null); + setModalOpen(true); + }; + + const handleCloseModal = () => { + setModalOpen(false); + setEditingDestino(null); + }; + + const handleSubmitModal = async (data: CreateOtroDestinoDto | (UpdateOtroDestinoDto & { idDestino: number })) => { + setApiErrorMessage(null); + try { + if (editingDestino && 'idDestino' in data) { + await otroDestinoService.updateOtroDestino(editingDestino.idDestino, data); + } else { + await otroDestinoService.createOtroDestino(data as CreateOtroDestinoDto); + } + cargarOtrosDestinos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Ocurrió un error inesperado al guardar el destino.'; + setApiErrorMessage(message); + throw err; + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm(`¿Está seguro de que desea eliminar este destino (ID: ${id})?`)) { + setApiErrorMessage(null); + try { + await otroDestinoService.deleteOtroDestino(id); + cargarOtrosDestinos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Ocurrió un error inesperado al eliminar el destino.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, destino: OtroDestinoDto) => { + setAnchorEl(event.currentTarget); + setSelectedDestinoRow(destino); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + setSelectedDestinoRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const displayData = otrosDestinos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeVer) { + return ( + + Gestionar Otros Destinos + {error || "No tiene permiso para acceder a esta sección."} + + ); + } + + return ( + + Gestionar Otros Destinos + + + setFiltroNombre(e.target.value)} + /> + + {puedeCrear && ( + + + + )} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && ( + + + + + Nombre + Observación + {(puedeModificar || puedeEliminar) && Acciones} + + + + {displayData.length === 0 && !loading ? ( + No se encontraron otros destinos. + ) : ( + displayData.map((destino) => ( + + {destino.nombre} + {destino.obs || '-'} + {(puedeModificar || puedeEliminar) && ( + + handleMenuOpen(e, destino)} disabled={!puedeModificar && !puedeEliminar}> + + + + )} + + )) + )} + +
+ +
+ )} + + + {puedeModificar && ( + { handleOpenModal(selectedDestinoRow!); handleMenuClose(); }}>Modificar + )} + {puedeEliminar && ( + handleDelete(selectedDestinoRow!.idDestino)}>Eliminar + )} + {(!puedeModificar && !puedeEliminar) && Sin acciones} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarOtrosDestinosPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarPreciosPublicacionPage.tsx b/Frontend/src/pages/Distribucion/GestionarPreciosPublicacionPage.tsx new file mode 100644 index 0000000..0e44152 --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarPreciosPublicacionPage.tsx @@ -0,0 +1,240 @@ +// src/pages/Distribucion/Publicaciones/GestionarPreciosPublicacionPage.tsx +import React, { useState, useEffect, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Box, Typography, Button, Paper, IconButton, Menu, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, + CircularProgress, Alert, Chip +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; + +import precioService from '../../services/Distribucion/precioService'; +import publicacionService from '../../services/Distribucion/publicacionService'; +import type { PrecioDto } from '../../models/dtos/Distribucion/PrecioDto'; +import type { CreatePrecioDto } from '../../models/dtos/Distribucion/CreatePrecioDto'; +import type { UpdatePrecioDto } from '../../models/dtos/Distribucion/UpdatePrecioDto'; +import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import PrecioFormModal from '../../components/Modals/Distribucion/PrecioFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarPreciosPublicacionPage: React.FC = () => { + const { idPublicacion: idPublicacionStr } = useParams<{ idPublicacion: string }>(); + const navigate = useNavigate(); + const idPublicacion = Number(idPublicacionStr); + + const [publicacion, setPublicacion] = useState(null); + const [precios, setPrecios] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [modalOpen, setModalOpen] = useState(false); + const [editingPrecio, setEditingPrecio] = useState(null); // Este estado determina si el modal edita + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedPrecioRow, setSelectedPrecioRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeGestionarPrecios = isSuperAdmin || tienePermiso("DP004"); + + const cargarDatos = useCallback(async () => { + if (isNaN(idPublicacion)) { + setError("ID de Publicación inválido."); + setLoading(false); + return; + } + if (!puedeGestionarPrecios) { + setError("No tiene permiso para gestionar precios."); + setLoading(false); + return; + } + + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const [pubData, preciosData] = await Promise.all([ + publicacionService.getPublicacionById(idPublicacion), + precioService.getPreciosPorPublicacion(idPublicacion) + ]); + setPublicacion(pubData); + setPrecios(preciosData); + } catch (err: any) { + console.error(err); + if (axios.isAxiosError(err) && err.response?.status === 404) { + setError(`Publicación con ID ${idPublicacion} no encontrada o sin acceso a sus precios.`); + } else { + setError('Error al cargar los datos de precios.'); + } + } finally { + setLoading(false); + } + }, [idPublicacion, puedeGestionarPrecios]); + + useEffect(() => { + cargarDatos(); + }, [cargarDatos]); + + const handleOpenModal = (precio?: PrecioDto) => { + setEditingPrecio(precio || null); // Si hay 'precio', el modal estará en modo edición + setApiErrorMessage(null); + setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); + setEditingPrecio(null); + }; + + // CORREGIDO: El segundo parámetro 'idPrecio' determina si es edición + const handleSubmitModal = async (data: CreatePrecioDto | UpdatePrecioDto, idPrecio?: number) => { + setApiErrorMessage(null); + try { + // Si idPrecio tiene valor, Y editingPrecio (initialData del modal) también lo tenía, es una actualización + if (idPrecio && editingPrecio) { + await precioService.updatePrecio(idPublicacion, idPrecio, data as UpdatePrecioDto); + } else { + await precioService.createPrecio(idPublicacion, data as CreatePrecioDto); + } + cargarDatos(); // Recargar lista + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el período de precio.'; + setApiErrorMessage(message); throw err; // Re-lanzar para que el modal maneje el estado de error + } + }; + + const handleDelete = async (idPrecio: number) => { + if (window.confirm(`¿Está seguro de eliminar este período de precio (ID: ${idPrecio})? Esta acción puede afectar la vigencia de períodos anteriores.`)) { + setApiErrorMessage(null); + try { + await precioService.deletePrecio(idPublicacion, idPrecio); + cargarDatos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el período de precio.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, precio: PrecioDto) => { + setAnchorEl(event.currentTarget); setSelectedPrecioRow(precio); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedPrecioRow(null); + }; + + const formatDate = (dateString?: string | null) => { + if (!dateString) return '-'; + const date = new Date(dateString); // Asegurar que se parsee correctamente si viene con hora + const day = String(date.getUTCDate()).padStart(2, '0'); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); // Meses son 0-indexados + const year = date.getUTCFullYear(); + return `${day}/${month}/${year}`; + }; + + + if (loading) { + return ; + } + if (error) { + return {error}; + } + if (!puedeGestionarPrecios) { + return Acceso denegado.; + } + + + return ( + + + + Gestionar Precios para: {publicacion?.nombre || 'Cargando...'} + + + Empresa: {publicacion?.nombreEmpresa || '-'} + + + + {puedeGestionarPrecios && ( + + )} + + + {apiErrorMessage && {apiErrorMessage}} + + + + + Vigencia DesdeVigencia Hasta + LunesMartes + MiércolesJueves + ViernesSábado + Domingo + Estado + Acciones + + + {precios.length === 0 ? ( + No hay períodos de precios definidos para esta publicación. + ) : ( + precios.map((p) => ( + + {formatDate(p.vigenciaD)} + {formatDate(p.vigenciaH)} + {p.lunes?.toFixed(2) || '-'} + {p.martes?.toFixed(2) || '-'} + {p.miercoles?.toFixed(2) || '-'} + {p.jueves?.toFixed(2) || '-'} + {p.viernes?.toFixed(2) || '-'} + {p.sabado?.toFixed(2) || '-'} + {p.domingo?.toFixed(2) || '-'} + + {!p.vigenciaH ? : } + + + handleMenuOpen(e, p)} disabled={!puedeGestionarPrecios}> + + + + + )))} + +
+
+ + + {puedeGestionarPrecios && selectedPrecioRow && ( + { handleOpenModal(selectedPrecioRow); handleMenuClose(); }}> + Editar Precios/Cerrar Período + + )} + {puedeGestionarPrecios && selectedPrecioRow && ( + handleDelete(selectedPrecioRow.idPrecio)}> + Eliminar Período + + )} + + + {idPublicacion && + setApiErrorMessage(null)} + /> + } +
+ ); +}; + +export default GestionarPreciosPublicacionPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx b/Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx new file mode 100644 index 0000000..d199f73 --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarPublicacionesPage.tsx @@ -0,0 +1,260 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, Chip, FormControl, InputLabel, Select, Tooltip, + FormControlLabel +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import publicacionService from '../../services/Distribucion/publicacionService'; +import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto'; +import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto'; +import PublicacionFormModal from '../../components/Modals/Distribucion/PublicacionFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; +import { useNavigate } from 'react-router-dom'; +import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; +import empresaService from '../../services/Distribucion/empresaService'; + + +const GestionarPublicacionesPage: React.FC = () => { + const [publicaciones, setPublicaciones] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [filtroNombre, setFiltroNombre] = useState(''); + const [filtroIdEmpresa, setFiltroIdEmpresa] = useState(''); + const [filtroSoloHabilitadas, setFiltroSoloHabilitadas] = useState(true); + const [empresas, setEmpresas] = useState([]); + const [loadingEmpresas, setLoadingEmpresas] = useState(false); + + + const [modalOpen, setModalOpen] = useState(false); + const [editingPublicacion, setEditingPublicacion] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedPublicacionRow, setSelectedPublicacionRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const navigate = useNavigate(); + + const puedeVer = isSuperAdmin || tienePermiso("DP001"); + const puedeCrear = isSuperAdmin || tienePermiso("DP002"); + const puedeModificar = isSuperAdmin || tienePermiso("DP003"); + const puedeGestionarPrecios = isSuperAdmin || tienePermiso("DP004"); + const puedeGestionarRecargos = isSuperAdmin || tienePermiso("DP005"); + const puedeEliminar = isSuperAdmin || tienePermiso("DP006"); + const puedeGestionarSecciones = isSuperAdmin || tienePermiso("DP007"); + + const fetchEmpresas = useCallback(async () => { + setLoadingEmpresas(true); + try { + const data = await empresaService.getAllEmpresas(); + setEmpresas(data); + } catch (err) { + console.error("Error cargando empresas para filtro:", err); + // Manejar error si es necesario, ej. mostrando un mensaje + } finally { + setLoadingEmpresas(false); + } + }, []); + + useEffect(() => { + fetchEmpresas(); + }, [fetchEmpresas]); + + + const cargarPublicaciones = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const idEmpresa = filtroIdEmpresa ? Number(filtroIdEmpresa) : undefined; + const data = await publicacionService.getAllPublicaciones(filtroNombre, idEmpresa, filtroSoloHabilitadas); + setPublicaciones(data); + } catch (err) { + console.error(err); setError('Error al cargar las publicaciones.'); + } finally { setLoading(false); } + }, [filtroNombre, filtroIdEmpresa, filtroSoloHabilitadas, puedeVer]); + + useEffect(() => { cargarPublicaciones(); }, [cargarPublicaciones]); + + const handleOpenModal = (publicacion?: PublicacionDto) => { + setEditingPublicacion(publicacion || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingPublicacion(null); + }; + + const handleSubmitModal = async (data: CreatePublicacionDto | UpdatePublicacionDto, id?: number) => { + setApiErrorMessage(null); + try { + if (id && editingPublicacion) { + await publicacionService.updatePublicacion(id, data as UpdatePublicacionDto); + } else { + await publicacionService.createPublicacion(data as CreatePublicacionDto); + } + cargarPublicaciones(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la publicación.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm(`¿Está seguro? Esta acción eliminará la publicación (ID: ${id}) y todas sus configuraciones asociadas (precios, recargos, secciones, etc.). ESTA ACCIÓN NO SE PUEDE DESHACER.`)) { + setApiErrorMessage(null); + try { + await publicacionService.deletePublicacion(id); + cargarPublicaciones(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar la publicación.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleToggleHabilitada = async (publicacion: PublicacionDto) => { + setApiErrorMessage(null); + const datosActualizados: UpdatePublicacionDto = { + nombre: publicacion.nombre, + observacion: publicacion.observacion, + idEmpresa: publicacion.idEmpresa, + ctrlDevoluciones: publicacion.ctrlDevoluciones, + habilitada: !publicacion.habilitada // Invertir estado + }; + try { + await publicacionService.updatePublicacion(publicacion.idPublicacion, datosActualizados); + cargarPublicaciones(); // Recargar para ver el cambio + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar estado de habilitación.'; + setApiErrorMessage(message); + } + }; + + + const handleMenuOpen = (event: React.MouseEvent, publicacion: PublicacionDto) => { + setAnchorEl(event.currentTarget); setSelectedPublicacionRow(publicacion); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedPublicacionRow(null); + }; + + // TODO: Implementar navegación a páginas de gestión de Precios, Recargos, Secciones + const handleNavigateToPrecios = (idPub: number) => { + navigate(`/distribucion/publicaciones/${idPub}/precios`); // Ruta anidada + handleMenuClose(); + }; + const handleNavigateToRecargos = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/recargos`); handleMenuClose(); }; + const handleNavigateToSecciones = (idPub: number) => { navigate(`/distribucion/publicaciones/${idPub}/secciones`); handleMenuClose(); }; + + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = publicaciones.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeVer) { + return {error || "No tiene permiso."}; + } + + return ( + + Gestionar Publicaciones + + + setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} /> + + Empresa + + + setFiltroSoloHabilitadas(e.target.checked)} size="small" />} + label="Solo Habilitadas" + /> + + {puedeCrear && ()} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + + + NombreEmpresa + Ctrl. Devol.Habilitada + Observación + Acciones + + + {displayData.length === 0 ? ( + No se encontraron publicaciones. + ) : ( + displayData.map((p) => ( + + {p.nombre}{p.nombreEmpresa} + {p.ctrlDevoluciones ? : } + + + handleToggleHabilitada(p)} size="small" disabled={!puedeModificar} /> + + + {p.observacion || '-'} + + handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar && !puedeGestionarPrecios && !puedeGestionarRecargos && !puedeGestionarSecciones}> + + + + + )))} + +
+ +
+ )} + + + {puedeModificar && ( { handleOpenModal(selectedPublicacionRow!); handleMenuClose(); }}>Modificar)} + {puedeGestionarPrecios && ( handleNavigateToPrecios(selectedPublicacionRow!.idPublicacion)}>Gestionar Precios)} + {puedeGestionarRecargos && ( handleNavigateToRecargos(selectedPublicacionRow!.idPublicacion)}>Gestionar Recargos)} + {puedeGestionarSecciones && ( handleNavigateToSecciones(selectedPublicacionRow!.idPublicacion)}>Gestionar Secciones)} + {puedeEliminar && ( handleDelete(selectedPublicacionRow!.idPublicacion)}>Eliminar)} + {/* Si no hay permisos para ninguna acción */} + {(!puedeModificar && !puedeEliminar && !puedeGestionarPrecios && !puedeGestionarRecargos && !puedeGestionarSecciones) && + Sin acciones} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarPublicacionesPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarZonasPage.tsx b/Frontend/src/pages/Distribucion/GestionarZonasPage.tsx index 1e03b4b..8526f40 100644 --- a/Frontend/src/pages/Distribucion/GestionarZonasPage.tsx +++ b/Frontend/src/pages/Distribucion/GestionarZonasPage.tsx @@ -6,11 +6,11 @@ import { } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import MoreVertIcon from '@mui/icons-material/MoreVert'; -import zonaService from '../../services/zonaService'; // Servicio de Zonas +import zonaService from '../../services/Distribucion/zonaService'; // Servicio de Zonas import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // DTO de Zonas import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; // DTOs Create import type { UpdateZonaDto } from '../../models/dtos/Zonas/UpdateZonaDto'; // DTOs Update -import ZonaFormModal from '../../components/Modals/ZonaFormModal'; // Modal de Zonas +import ZonaFormModal from '../../components/Modals/Distribucion/ZonaFormModal'; // Modal de Zonas import { usePermissions } from '../../hooks/usePermissions'; // Hook de permisos import axios from 'axios'; // Para manejo de errores diff --git a/Frontend/src/pages/Distribucion/OtrosDestinosPage.tsx b/Frontend/src/pages/Distribucion/OtrosDestinosPage.tsx deleted file mode 100644 index 61d7ccf..0000000 --- a/Frontend/src/pages/Distribucion/OtrosDestinosPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; - -const OtrosDestinosPage: React.FC = () => { - return Página de Gestión de Otros Destinos; -}; -export default OtrosDestinosPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/PublicacionesPage.tsx b/Frontend/src/pages/Distribucion/PublicacionesPage.tsx deleted file mode 100644 index 9fc64d3..0000000 --- a/Frontend/src/pages/Distribucion/PublicacionesPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; - -const PublicacionesPage: React.FC = () => { - return Página de Gestión de Publicaciones; -}; -export default PublicacionesPage; \ No newline at end of file diff --git a/Frontend/src/pages/Impresion/GestionarEstadosBobinaPage.tsx b/Frontend/src/pages/Impresion/GestionarEstadosBobinaPage.tsx index e7cabd9..8cbf3ea 100644 --- a/Frontend/src/pages/Impresion/GestionarEstadosBobinaPage.tsx +++ b/Frontend/src/pages/Impresion/GestionarEstadosBobinaPage.tsx @@ -6,11 +6,11 @@ import { } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import MoreVertIcon from '@mui/icons-material/MoreVert'; -import estadoBobinaService from '../../services/estadoBobinaService'; +import estadoBobinaService from '../../services/Impresion/estadoBobinaService'; import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto'; import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto'; -import EstadoBobinaFormModal from '../../components/Modals/EstadoBobinaFormModal'; +import EstadoBobinaFormModal from '../../components/Modals/Impresion/EstadoBobinaFormModal'; import { usePermissions } from '../../hooks/usePermissions'; import axios from 'axios'; diff --git a/Frontend/src/pages/Impresion/GestionarPlantasPage.tsx b/Frontend/src/pages/Impresion/GestionarPlantasPage.tsx index 6305286..fc9adb2 100644 --- a/Frontend/src/pages/Impresion/GestionarPlantasPage.tsx +++ b/Frontend/src/pages/Impresion/GestionarPlantasPage.tsx @@ -6,11 +6,11 @@ import { } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import MoreVertIcon from '@mui/icons-material/MoreVert'; -import plantaService from '../../services/plantaService'; // Servicio de Plantas +import plantaService from '../../services/Impresion/plantaService'; // Servicio de Plantas import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; import type { CreatePlantaDto } from '../../models/dtos/Impresion/CreatePlantaDto'; import type { UpdatePlantaDto } from '../../models/dtos/Impresion/UpdatePlantaDto'; -import PlantaFormModal from '../../components/Modals/PlantaFormModal'; // Modal de Plantas +import PlantaFormModal from '../../components/Modals/Impresion/PlantaFormModal'; // Modal de Plantas import { usePermissions } from '../../hooks/usePermissions'; import axios from 'axios'; diff --git a/Frontend/src/pages/Impresion/GestionarTiposBobinaPage.tsx b/Frontend/src/pages/Impresion/GestionarTiposBobinaPage.tsx index e7a9be6..33ce14b 100644 --- a/Frontend/src/pages/Impresion/GestionarTiposBobinaPage.tsx +++ b/Frontend/src/pages/Impresion/GestionarTiposBobinaPage.tsx @@ -6,11 +6,11 @@ import { } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import MoreVertIcon from '@mui/icons-material/MoreVert'; -import tipoBobinaService from '../../services/tipoBobinaService'; // Servicio específico +import tipoBobinaService from '../../services/Impresion/tipoBobinaService'; // Servicio específico import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; import type { CreateTipoBobinaDto } from '../../models/dtos/Impresion/CreateTipoBobinaDto'; import type { UpdateTipoBobinaDto } from '../../models/dtos/Impresion/UpdateTipoBobinaDto'; -import TipoBobinaFormModal from '../../components/Modals/TipoBobinaFormModal'; // Modal específico +import TipoBobinaFormModal from '../../components/Modals/Impresion/TipoBobinaFormModal'; // Modal específico import { usePermissions } from '../../hooks/usePermissions'; import axios from 'axios'; diff --git a/Frontend/src/pages/LoginPage.tsx b/Frontend/src/pages/LoginPage.tsx index edaba4e..673e7b8 100644 --- a/Frontend/src/pages/LoginPage.tsx +++ b/Frontend/src/pages/LoginPage.tsx @@ -1,11 +1,11 @@ import React, { useState } from 'react'; import axios from 'axios'; // Importar axios import { useAuth } from '../contexts/AuthContext'; -import type { LoginRequestDto } from '../models/dtos/LoginRequestDto'; // Usar type +import type { LoginRequestDto } from '../models/dtos/Usuarios/LoginRequestDto'; // Usar type // Importaciones de Material UI import { Container, TextField, Button, Typography, Box, Alert } from '@mui/material'; -import authService from '../services/authService'; +import authService from '../services/Usuarios/authService'; import logo from '../assets/eldia.png'; diff --git a/Frontend/src/pages/Usuarios/AsignarPermisosAPerfilPage.tsx b/Frontend/src/pages/Usuarios/AsignarPermisosAPerfilPage.tsx new file mode 100644 index 0000000..1e1f132 --- /dev/null +++ b/Frontend/src/pages/Usuarios/AsignarPermisosAPerfilPage.tsx @@ -0,0 +1,159 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Box, Typography, Button, Paper, CircularProgress, Alert, + Checkbox, FormControlLabel, FormGroup // Para el caso sin componente checklist +} from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import SaveIcon from '@mui/icons-material/Save'; +import perfilService from '../../services/Usuarios/perfilService'; +import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto'; +import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto'; +import { usePermissions } from '../../hooks/usePermissions'; // Para verificar si el usuario actual puede estar aquí +import axios from 'axios'; +import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist'; // Importar el componente + +const AsignarPermisosAPerfilPage: React.FC = () => { + const { idPerfil } = useParams<{ idPerfil: string }>(); + const navigate = useNavigate(); + const { tienePermiso, isSuperAdmin } = usePermissions(); + + const puedeAsignar = isSuperAdmin || tienePermiso("PU004"); + + const [perfil, setPerfil] = useState(null); + const [permisosDisponibles, setPermisosDisponibles] = useState([]); + // Usamos un Set para los IDs de los permisos seleccionados para eficiencia + const [permisosSeleccionados, setPermisosSeleccionados] = useState>(new Set()); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + const idPerfilNum = Number(idPerfil); + + const cargarDatos = useCallback(async () => { + if (!puedeAsignar) { + setError("Acceso denegado. No tiene permiso para asignar permisos."); + setLoading(false); + return; + } + if (isNaN(idPerfilNum)) { + setError("ID de Perfil inválido."); + setLoading(false); + return; + } + setLoading(true); setError(null); setSuccessMessage(null); + try { + const [perfilData, permisosData] = await Promise.all([ + perfilService.getPerfilById(idPerfilNum), + perfilService.getPermisosPorPerfil(idPerfilNum) + ]); + setPerfil(perfilData); + setPermisosDisponibles(permisosData); + // Inicializar los permisos seleccionados basados en los que vienen 'asignado: true' + setPermisosSeleccionados(new Set(permisosData.filter(p => p.asignado).map(p => p.id))); + } catch (err) { + console.error(err); + setError('Error al cargar datos del perfil o permisos.'); + if (axios.isAxiosError(err) && err.response?.status === 404) { + setError(`Perfil con ID ${idPerfilNum} no encontrado.`); + } + } finally { + setLoading(false); + } + }, [idPerfilNum, puedeAsignar]); + + useEffect(() => { + cargarDatos(); + }, [cargarDatos]); + + const handlePermisoChange = (permisoId: number, asignado: boolean) => { + setPermisosSeleccionados(prev => { + const next = new Set(prev); + if (asignado) { + next.add(permisoId); + } else { + next.delete(permisoId); + } + return next; + }); + // Limpiar mensajes al cambiar selección + if (successMessage) setSuccessMessage(null); + if (error) setError(null); + }; + + const handleGuardarCambios = async () => { + if (!puedeAsignar || !perfil) return; + setSaving(true); setError(null); setSuccessMessage(null); + try { + await perfilService.updatePermisosPorPerfil(perfil.id, { + permisosIds: Array.from(permisosSeleccionados) + }); + setSuccessMessage('Permisos actualizados correctamente.'); + // Opcional: recargar datos, aunque el estado local ya está actualizado + // cargarDatos(); + } catch (err: any) { + console.error(err); + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Error al guardar los permisos.'; + setError(message); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ; + } + + if (error && !perfil) { // Si hay un error crítico al cargar el perfil + return {error}; + } + if (!puedeAsignar) { + return Acceso denegado.; + } + if (!perfil) { // Si no hay error, pero el perfil es null después de cargar (no debería pasar si no hay error) + return Perfil no encontrado.; + } + + + return ( + + + + Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'} + + + ID Perfil: {perfil?.id} + + + {error && {error}} + {successMessage && {successMessage}} + + + + + + + + + ); +}; + +export default AsignarPermisosAPerfilPage; \ No newline at end of file diff --git a/Frontend/src/pages/ChangePasswordPagePlaceholder.tsx b/Frontend/src/pages/Usuarios/ChangePasswordPagePlaceholder.tsx similarity index 92% rename from Frontend/src/pages/ChangePasswordPagePlaceholder.tsx rename to Frontend/src/pages/Usuarios/ChangePasswordPagePlaceholder.tsx index 0e48a65..2a9b961 100644 --- a/Frontend/src/pages/ChangePasswordPagePlaceholder.tsx +++ b/Frontend/src/pages/Usuarios/ChangePasswordPagePlaceholder.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Typography, Container, Button } from '@mui/material'; -import { useAuth } from '../contexts/AuthContext'; +import { useAuth } from '../../contexts/AuthContext'; const ChangePasswordPagePlaceholder: React.FC = () => { const { setShowForcedPasswordChangeModal } = useAuth(); diff --git a/Frontend/src/pages/Usuarios/GestionarPerfilesPage.tsx b/Frontend/src/pages/Usuarios/GestionarPerfilesPage.tsx new file mode 100644 index 0000000..e9a6a16 --- /dev/null +++ b/Frontend/src/pages/Usuarios/GestionarPerfilesPage.tsx @@ -0,0 +1,237 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, Tooltip // Añadir Tooltip +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; // Para asignar permisos +import perfilService from '../../services/Usuarios/perfilService'; +import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto'; +import type { CreatePerfilDto } from '../../models/dtos/Usuarios/CreatePerfilDto'; +import type { UpdatePerfilDto } from '../../models/dtos/Usuarios/UpdatePerfilDto'; +import PerfilFormModal from '../../components/Modals/Usuarios/PerfilFormModal'; +// import PermisosPorPerfilModal from '../../components/Modals/PermisosPorPerfilModal'; // Lo crearemos después +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; +import { useNavigate } from 'react-router-dom'; // Para navegar + +const GestionarPerfilesPage: React.FC = () => { + const [perfiles, setPerfiles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroNombre, setFiltroNombre] = useState(''); + + const [modalOpen, setModalOpen] = useState(false); + const [editingPerfil, setEditingPerfil] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + // const [permisosModalOpen, setPermisosModalOpen] = useState(false); // Para modal de permisos + // const [selectedPerfilForPermisos, setSelectedPerfilForPermisos] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedPerfilRow, setSelectedPerfilRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const navigate = useNavigate(); // Hook para navegación + + // Permisos para Perfiles (PU001 a PU004) + const puedeVer = isSuperAdmin || tienePermiso("PU001"); + const puedeCrear = isSuperAdmin || tienePermiso("PU002"); + const puedeModificar = isSuperAdmin || tienePermiso("PU003"); // Modificar nombre/desc + const puedeEliminar = isSuperAdmin || tienePermiso("PU003"); // Excel dice PU003 para eliminar + const puedeAsignarPermisos = isSuperAdmin || tienePermiso("PU004"); + + const cargarPerfiles = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); + setLoading(false); + return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const data = await perfilService.getAllPerfiles(filtroNombre); + setPerfiles(data); + } catch (err) { + console.error(err); setError('Error al cargar los perfiles.'); + } finally { setLoading(false); } + }, [filtroNombre, puedeVer]); + + useEffect(() => { cargarPerfiles(); }, [cargarPerfiles]); + + const handleOpenModal = (perfil?: PerfilDto) => { + setEditingPerfil(perfil || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingPerfil(null); + }; + + const handleSubmitModal = async (data: CreatePerfilDto | (UpdatePerfilDto & { id: number })) => { + setApiErrorMessage(null); + try { + if (editingPerfil && 'id' in data) { + await perfilService.updatePerfil(editingPerfil.id, data); + } else { + await perfilService.createPerfil(data as CreatePerfilDto); + } + cargarPerfiles(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el perfil.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm(`¿Está seguro? ID: ${id}`)) { + setApiErrorMessage(null); + try { + await perfilService.deletePerfil(id); + cargarPerfiles(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el perfil.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, perfil: PerfilDto) => { + setAnchorEl(event.currentTarget); setSelectedPerfilRow(perfil); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedPerfilRow(null); + }; + + const handleOpenPermisosModal = (perfil: PerfilDto) => { + // setSelectedPerfilForPermisos(perfil); + // setPermisosModalOpen(true); + handleMenuClose(); + // Navegar a la página de asignación de permisos + navigate(`/usuarios/perfiles/${perfil.id}/permisos`); + }; + // const handleClosePermisosModal = () => { + // setPermisosModalOpen(false); setSelectedPerfilForPermisos(null); + // }; + // const handleSubmitPermisos = async (idPerfil: number, permisosIds: number[]) => { + // try { + // // await perfilService.updatePermisosPorPerfil(idPerfil, permisosIds); + // // console.log("Permisos actualizados para perfil:", idPerfil); + // // Quizás un snackbar de éxito + // } catch (error) { + // console.error("Error al actualizar permisos:", error); + // setApiErrorMessage("Error al actualizar permisos."); + // } + // handleClosePermisosModal(); + // }; + + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = perfiles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeVer) { + return ( + + Gestionar Perfiles + {error || "No tiene permiso para acceder a esta sección."} + + ); + } + + return ( + + Gestionar Perfiles + + + setFiltroNombre(e.target.value)} /> + + {puedeCrear && ( + + + + )} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && ( + + + + + Nombre del Perfil + Descripción + {(puedeModificar || puedeEliminar || puedeAsignarPermisos) && Acciones} + + + + {displayData.length === 0 && !loading ? ( + No se encontraron perfiles. + ) : ( + displayData.map((perfil) => ( + + {perfil.nombrePerfil} + {perfil.descripcion || '-'} + {(puedeModificar || puedeEliminar || puedeAsignarPermisos) && ( + + handleMenuOpen(e, perfil)} disabled={!puedeModificar && !puedeEliminar && !puedeAsignarPermisos}> + + + + )} + + )) + )} + +
+ +
+ )} + + + {puedeModificar && ( + { handleOpenModal(selectedPerfilRow!); handleMenuClose(); }}>Modificar + )} + {puedeEliminar && ( + handleDelete(selectedPerfilRow!.id)}>Eliminar + )} + {puedeAsignarPermisos && ( + handleOpenPermisosModal(selectedPerfilRow!)}>Asignar Permisos + )} + {(!puedeModificar && !puedeEliminar && !puedeAsignarPermisos) && Sin acciones} + + + setApiErrorMessage(null)} + /> + {/* {selectedPerfilForPermisos && ( + []} // Implementar esto + /> + )} */} +
+ ); +}; + +export default GestionarPerfilesPage; \ No newline at end of file diff --git a/Frontend/src/pages/Usuarios/GestionarPermisosPage.tsx b/Frontend/src/pages/Usuarios/GestionarPermisosPage.tsx new file mode 100644 index 0000000..2b77a12 --- /dev/null +++ b/Frontend/src/pages/Usuarios/GestionarPermisosPage.tsx @@ -0,0 +1,200 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import permisoService from '../../services/Usuarios/permisoService'; +import type { PermisoDto } from '../../models/dtos/Usuarios/PermisoDto'; +import type { CreatePermisoDto } from '../../models/dtos/Usuarios/CreatePermisoDto'; +import type { UpdatePermisoDto } from '../../models/dtos/Usuarios/UpdatePermisoDto'; +import PermisoFormModal from '../../components/Modals/Usuarios/PermisoFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarPermisosPage: React.FC = () => { + const [permisos, setPermisos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroModulo, setFiltroModulo] = useState(''); + const [filtroCodAcc, setFiltroCodAcc] = useState(''); + + const [modalOpen, setModalOpen] = useState(false); + const [editingPermiso, setEditingPermiso] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); // Un poco más para esta tabla + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedPermisoRow, setSelectedPermisoRow] = useState(null); + + const { isSuperAdmin } = usePermissions(); // Solo SuperAdmin puede acceder + + const cargarPermisos = useCallback(async () => { + if (!isSuperAdmin) { + setError("Acceso denegado. Solo SuperAdmin puede gestionar permisos."); + setLoading(false); + return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const data = await permisoService.getAllPermisos(filtroModulo, filtroCodAcc); + setPermisos(data); + } catch (err) { + console.error(err); setError('Error al cargar los permisos.'); + } finally { setLoading(false); } + }, [filtroModulo, filtroCodAcc, isSuperAdmin]); + + useEffect(() => { cargarPermisos(); }, [cargarPermisos]); + + const handleOpenModal = (permiso?: PermisoDto) => { + setEditingPermiso(permiso || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingPermiso(null); + }; + + const handleSubmitModal = async (data: CreatePermisoDto | (UpdatePermisoDto & { id: number })) => { + setApiErrorMessage(null); + try { + if (editingPermiso && 'id' in data) { + await permisoService.updatePermiso(editingPermiso.id, data); + } else { + await permisoService.createPermiso(data as CreatePermisoDto); + } + cargarPermisos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el permiso.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm(`¿Está seguro de eliminar este permiso (ID: ${id})?`)) { + setApiErrorMessage(null); + try { + await permisoService.deletePermiso(id); + cargarPermisos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el permiso.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, permiso: PermisoDto) => { + setAnchorEl(event.currentTarget); setSelectedPermisoRow(permiso); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedPermisoRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = permisos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !isSuperAdmin) { + return ( + + Definición de Permisos + {error || "Acceso denegado."} + + ); + } + + return ( + + Definición de Permisos (SuperAdmin) + + + setFiltroModulo(e.target.value)} + sx={{ flexGrow: 1, minWidth: '200px' }} // Para que se adapte mejor + /> + setFiltroCodAcc(e.target.value)} + sx={{ flexGrow: 1, minWidth: '200px' }} + /> + {/* El botón de búsqueda es opcional si el filtro es en tiempo real */} + {/* */} + + {isSuperAdmin && ( + + + + )} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && isSuperAdmin && ( + + + + + Módulo + Descripción + CodAcc + Acciones + + + + {displayData.length === 0 && !loading ? ( + No se encontraron permisos. + ) : ( + displayData.map((permiso) => ( + + {permiso.modulo} + {permiso.descPermiso} + {permiso.codAcc} + + handleMenuOpen(e, permiso)}> + + + + + )) + )} + +
+ +
+ )} + + + { handleOpenModal(selectedPermisoRow!); handleMenuClose(); }}>Modificar + handleDelete(selectedPermisoRow!.id)}>Eliminar + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarPermisosPage; \ No newline at end of file diff --git a/Frontend/src/pages/Usuarios/GestionarUsuariosPage.tsx b/Frontend/src/pages/Usuarios/GestionarUsuariosPage.tsx new file mode 100644 index 0000000..90fc073 --- /dev/null +++ b/Frontend/src/pages/Usuarios/GestionarUsuariosPage.tsx @@ -0,0 +1,264 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, Tooltip +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import VpnKeyIcon from '@mui/icons-material/VpnKey'; // Para resetear clave +import usuarioService from '../../services/Usuarios/usuarioService'; +import type { UsuarioDto } from '../../models/dtos/Usuarios/UsuarioDto'; +import type { CreateUsuarioRequestDto } from '../../models/dtos/Usuarios/CreateUsuarioRequestDto'; +import type { UpdateUsuarioRequestDto } from '../../models/dtos/Usuarios/UpdateUsuarioRequestDto'; +import type { SetPasswordRequestDto } from '../../models/dtos/Usuarios/SetPasswordRequestDto'; +import UsuarioFormModal from '../../components/Modals/Usuarios/UsuarioFormModal'; +import SetPasswordModal from '../../components/Modals/Usuarios/SetPasswordModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarUsuariosPage: React.FC = () => { + const [usuarios, setUsuarios] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroUser, setFiltroUser] = useState(''); + const [filtroNombre, setFiltroNombre] = useState(''); + + const [usuarioModalOpen, setUsuarioModalOpen] = useState(false); + const [editingUsuario, setEditingUsuario] = useState(null); + + const [setPasswordModalOpen, setSetPasswordModalOpen] = useState(false); + const [selectedUsuarioForPassword, setSelectedUsuarioForPassword] = useState(null); + + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedUsuarioRow, setSelectedUsuarioRow] = useState(null); + + const { tienePermiso, isSuperAdmin, currentUser } = usePermissions(); + + const puedeVer = isSuperAdmin || tienePermiso("CU001"); + const puedeCrear = isSuperAdmin || tienePermiso("CU002"); + const puedeModificar = isSuperAdmin || tienePermiso("CU003"); // Modificar datos básicos + const puedeAsignarPerfil = isSuperAdmin || tienePermiso("CU004"); // Modificar perfil + // Resetear clave es típicamente SuperAdmin + const puedeResetearClave = isSuperAdmin; + + const cargarUsuarios = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); + setLoading(false); + return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const data = await usuarioService.getAllUsuarios(filtroUser, filtroNombre); + setUsuarios(data); + } catch (err) { + console.error(err); setError('Error al cargar los usuarios.'); + } finally { setLoading(false); } + }, [filtroUser, filtroNombre, puedeVer]); + + useEffect(() => { cargarUsuarios(); }, [cargarUsuarios]); + + const handleOpenUsuarioModal = (usuario?: UsuarioDto) => { + setEditingUsuario(usuario || null); setApiErrorMessage(null); setUsuarioModalOpen(true); + }; + const handleCloseUsuarioModal = () => { + setUsuarioModalOpen(false); setEditingUsuario(null); + }; + + const handleSubmitUsuarioModal = async (data: CreateUsuarioRequestDto | UpdateUsuarioRequestDto, id?: number) => { + setApiErrorMessage(null); + try { + if (id && editingUsuario) { // Es Update + await usuarioService.updateUsuario(id, data as UpdateUsuarioRequestDto); + } else { // Es Create + await usuarioService.createUsuario(data as CreateUsuarioRequestDto); + } + cargarUsuarios(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el usuario.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleOpenSetPasswordModal = (usuario: UsuarioDto) => { + setSelectedUsuarioForPassword(usuario); + setApiErrorMessage(null); + setSetPasswordModalOpen(true); + handleMenuClose(); + }; + const handleCloseSetPasswordModal = () => { + setSetPasswordModalOpen(false); setSelectedUsuarioForPassword(null); + }; + const handleSubmitSetPassword = async (userId: number, data: SetPasswordRequestDto) => { + setApiErrorMessage(null); + try { + await usuarioService.setPassword(userId, data); + cargarUsuarios(); // Para reflejar el cambio en 'DebeCambiarClave' + } catch (err:any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al establecer la contraseña.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleToggleHabilitado = async (usuario: UsuarioDto) => { + setApiErrorMessage(null); + // Un usuario no puede deshabilitarse a sí mismo + if (currentUser?.userId === usuario.id) { + setApiErrorMessage("No puede cambiar el estado de habilitación de su propio usuario."); + return; + } + try { + await usuarioService.toggleHabilitado(usuario.id, !usuario.habilitada); + cargarUsuarios(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar el estado del usuario.'; + setApiErrorMessage(message); + } + }; + + + const handleMenuOpen = (event: React.MouseEvent, usuario: UsuarioDto) => { + setAnchorEl(event.currentTarget); setSelectedUsuarioRow(usuario); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedUsuarioRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = usuarios.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeVer) { + return {error || "No tiene permiso para acceder a esta sección."}; + } + + return ( + + Gestionar Usuarios + + {/* SECCIÓN DE FILTROS CORREGIDA */} + + setFiltroUser(e.target.value)} + sx={{ flexGrow: 1, minWidth: '200px' }} + /> + setFiltroNombre(e.target.value)} + sx={{ flexGrow: 1, minWidth: '200px' }} + /> + + {puedeCrear && ( + + + + )} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + + + + Usuario + Nombre Completo + Perfil + Habilitado + Cambiar Clave + SuperAdmin + Acciones + + + + {displayData.length === 0 && !loading ? ( + No se encontraron usuarios. + ) : ( + displayData.map((usr) => ( + + {usr.user} + {`${usr.nombre} ${usr.apellido}`} + {usr.nombrePerfil} + + + handleToggleHabilitado(usr)} + disabled={!puedeModificar || currentUser?.userId === usr.id} + size="small" + /> + + + {usr.debeCambiarClave ? 'Sí' : 'No'} + {usr.supAdmin ? 'Sí' : 'No'} + + handleMenuOpen(e, usr)} disabled={!puedeModificar && !puedeAsignarPerfil && !puedeResetearClave}> + + + + + )) + )} + +
+ +
+ )} + + + {(puedeModificar || puedeAsignarPerfil) && ( + { handleOpenUsuarioModal(selectedUsuarioRow!); handleMenuClose(); }}>Modificar + )} + {puedeResetearClave && selectedUsuarioRow && currentUser?.userId !== selectedUsuarioRow.id && ( + handleOpenSetPasswordModal(selectedUsuarioRow!)}> + Resetear Contraseña + + )} + {/* No hay "Eliminar" directo, se usa el switch de Habilitado */} + {(!puedeModificar && !puedeAsignarPerfil && !puedeResetearClave) && Sin acciones} + + + setApiErrorMessage(null)} + /> + {selectedUsuarioForPassword && ( + setApiErrorMessage(null)} + /> + )} +
+ ); +}; + +export default GestionarUsuariosPage; \ No newline at end of file diff --git a/Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx b/Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx new file mode 100644 index 0000000..9f8c81e --- /dev/null +++ b/Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx @@ -0,0 +1,68 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Tabs, Tab, Paper, Typography } from '@mui/material'; +import { Outlet, useNavigate, useLocation } from 'react-router-dom'; + +const usuariosSubModules = [ + { label: 'Perfiles', path: 'perfiles' }, + { label: 'Permisos (Definición)', path: 'permisos' }, + { label: 'Usuarios', path: 'gestion-usuarios' }, +]; + +const UsuariosIndexPage: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const [selectedSubTab, setSelectedSubTab] = useState(false); + + useEffect(() => { + const currentBasePath = '/usuarios'; + const subPath = location.pathname.startsWith(currentBasePath + '/') + ? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Tomar solo la primera parte de la subruta + : (location.pathname === currentBasePath ? usuariosSubModules[0]?.path : undefined); + + const activeTabIndex = usuariosSubModules.findIndex( + (subModule) => subModule.path === subPath + ); + + if (activeTabIndex !== -1) { + setSelectedSubTab(activeTabIndex); + } else { + if (location.pathname === currentBasePath && usuariosSubModules.length > 0) { + navigate(usuariosSubModules[0].path, { replace: true }); + setSelectedSubTab(0); + } else { + setSelectedSubTab(false); + } + } + }, [location.pathname, navigate]); + + const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { + setSelectedSubTab(newValue); + navigate(usuariosSubModules[newValue].path); + }; + + return ( + + Módulo de Usuarios y Seguridad + + + {usuariosSubModules.map((subModule) => ( + + ))} + + + + + + + ); +}; + +export default UsuariosIndexPage; \ No newline at end of file diff --git a/Frontend/src/routes/AppRoutes.tsx b/Frontend/src/routes/AppRoutes.tsx index dce4dcd..5a9d6e3 100644 --- a/Frontend/src/routes/AppRoutes.tsx +++ b/Frontend/src/routes/AppRoutes.tsx @@ -13,10 +13,11 @@ import ESCanillasPage from '../pages/Distribucion/ESCanillasPage'; import ControlDevolucionesPage from '../pages/Distribucion/ControlDevolucionesPage'; import ESDistribuidoresPage from '../pages/Distribucion/ESDistribuidoresPage'; import SalidasOtrosDestinosPage from '../pages/Distribucion/SalidasOtrosDestinosPage'; -import CanillasPage from '../pages/Distribucion/CanillasPage'; -import DistribuidoresPage from '../pages/Distribucion/DistribuidoresPage'; -import PublicacionesPage from '../pages/Distribucion/PublicacionesPage'; -import OtrosDestinosPage from '../pages/Distribucion/OtrosDestinosPage'; +import GestionarCanillitasPage from '../pages/Distribucion/GestionarCanillitasPage'; +import GestionarDistribuidoresPage from '../pages/Distribucion/GestionarDistribuidoresPage'; +import GestionarPublicacionesPage from '../pages/Distribucion/GestionarPublicacionesPage'; // Ajusta la ruta si la moviste +import GestionarPreciosPublicacionPage from '../pages/Distribucion/GestionarPreciosPublicacionPage'; +import GestionarOtrosDestinosPage from '../pages/Distribucion/GestionarOtrosDestinosPage'; import GestionarZonasPage from '../pages/Distribucion/GestionarZonasPage'; import GestionarEmpresasPage from '../pages/Distribucion/GestionarEmpresasPage'; @@ -30,6 +31,13 @@ import GestionarEstadosBobinaPage from '../pages/Impresion/GestionarEstadosBobin import ContablesIndexPage from '../pages/Contables/ContablesIndexPage'; import GestionarTiposPagoPage from '../pages/Contables/GestionarTiposPagoPage'; +// Usuarios +import UsuariosIndexPage from '../pages/Usuarios/UsuariosIndexPage'; // Crear este componente +import GestionarPerfilesPage from '../pages/Usuarios/GestionarPerfilesPage'; +import GestionarPermisosPage from '../pages/Usuarios/GestionarPermisosPage'; +import AsignarPermisosAPerfilPage from '../pages/Usuarios/AsignarPermisosAPerfilPage'; +import GestionarUsuariosPage from '../pages/Usuarios/GestionarUsuariosPage'; + // --- ProtectedRoute y PublicRoute SIN CAMBIOS --- const ProtectedRoute: React.FC<{ children: JSX.Element }> = ({ children }) => { const { isAuthenticated, isLoading } = useAuth(); @@ -91,12 +99,13 @@ const AppRoutes = () => { } /> } /> } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> } /> - } /> + } /> {/* Módulo Contable (anidado) */} @@ -118,7 +127,15 @@ const AppRoutes = () => { } /> } /> } /> - {/* } /> */} + + {/* Módulo de Usuarios (anidado) */} + }> + } /> {/* Redirigir a la primera subpestaña */} + } /> + } /> + } /> + } /> + {/* Ruta catch-all DENTRO del layout protegido */} } /> diff --git a/Frontend/src/services/tipoPagoService.ts b/Frontend/src/services/Contables/tipoPagoService.ts similarity index 81% rename from Frontend/src/services/tipoPagoService.ts rename to Frontend/src/services/Contables/tipoPagoService.ts index 28073cf..846d02d 100644 --- a/Frontend/src/services/tipoPagoService.ts +++ b/Frontend/src/services/Contables/tipoPagoService.ts @@ -1,7 +1,7 @@ -import apiClient from './apiClient'; -import type { TipoPago } from '../models/Entities/TipoPago'; -import type { CreateTipoPagoDto } from '../models/dtos/tiposPago/CreateTipoPagoDto'; -import type { UpdateTipoPagoDto } from '../models/dtos/tiposPago/UpdateTipoPagoDto'; +import apiClient from '../apiClient'; +import type { TipoPago } from '../../models/Entities/TipoPago'; +import type { CreateTipoPagoDto } from '../../models/dtos/tiposPago/CreateTipoPagoDto'; +import type { UpdateTipoPagoDto } from '../../models/dtos/tiposPago/UpdateTipoPagoDto'; const getAllTiposPago = async (nombreFilter?: string): Promise => { const params: Record = {}; diff --git a/Frontend/src/services/Distribucion/canillaService.ts b/Frontend/src/services/Distribucion/canillaService.ts new file mode 100644 index 0000000..3fb8e06 --- /dev/null +++ b/Frontend/src/services/Distribucion/canillaService.ts @@ -0,0 +1,46 @@ +import apiClient from '../apiClient'; +import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; +import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto'; +import type { UpdateCanillaDto } from '../../models/dtos/Distribucion/UpdateCanillaDto'; +import type { ToggleBajaCanillaDto } from '../../models/dtos/Distribucion/ToggleBajaCanillaDto'; + + +const getAllCanillas = async (nomApeFilter?: string, legajoFilter?: number, soloActivos?: boolean): Promise => { + const params: Record = {}; + if (nomApeFilter) params.nomApe = nomApeFilter; + if (legajoFilter !== undefined && legajoFilter !== null) params.legajo = legajoFilter; + if (soloActivos !== undefined) params.soloActivos = soloActivos; + + const response = await apiClient.get('/canillas', { params }); + return response.data; +}; + +const getCanillaById = async (id: number): Promise => { + const response = await apiClient.get(`/canillas/${id}`); + return response.data; +}; + +const createCanilla = async (data: CreateCanillaDto): Promise => { + const response = await apiClient.post('/canillas', data); + return response.data; +}; + +const updateCanilla = async (id: number, data: UpdateCanillaDto): Promise => { + await apiClient.put(`/canillas/${id}`, data); +}; + +const toggleBajaCanilla = async (id: number, data: ToggleBajaCanillaDto): Promise => { + // El backend espera el DTO en el cuerpo para este endpoint específico. + await apiClient.post(`/canillas/${id}/toggle-baja`, data); +}; + + +const canillaService = { + getAllCanillas, + getCanillaById, + createCanilla, + updateCanilla, + toggleBajaCanilla, +}; + +export default canillaService; \ No newline at end of file diff --git a/Frontend/src/services/Distribucion/distribuidorService.ts b/Frontend/src/services/Distribucion/distribuidorService.ts new file mode 100644 index 0000000..d6a1f73 --- /dev/null +++ b/Frontend/src/services/Distribucion/distribuidorService.ts @@ -0,0 +1,41 @@ +import apiClient from '../apiClient'; +import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; +import type { CreateDistribuidorDto } from '../../models/dtos/Distribucion/CreateDistribuidorDto'; +import type { UpdateDistribuidorDto } from '../../models/dtos/Distribucion/UpdateDistribuidorDto'; + +const getAllDistribuidores = async (nombreFilter?: string, nroDocFilter?: string): Promise => { + const params: Record = {}; + if (nombreFilter) params.nombre = nombreFilter; + if (nroDocFilter) params.nroDoc = nroDocFilter; + + const response = await apiClient.get('/distribuidores', { params }); + return response.data; +}; + +const getDistribuidorById = async (id: number): Promise => { + const response = await apiClient.get(`/distribuidores/${id}`); + return response.data; +}; + +const createDistribuidor = async (data: CreateDistribuidorDto): Promise => { + const response = await apiClient.post('/distribuidores', data); + return response.data; +}; + +const updateDistribuidor = async (id: number, data: UpdateDistribuidorDto): Promise => { + await apiClient.put(`/distribuidores/${id}`, data); +}; + +const deleteDistribuidor = async (id: number): Promise => { + await apiClient.delete(`/distribuidores/${id}`); +}; + +const distribuidorService = { + getAllDistribuidores, + getDistribuidorById, + createDistribuidor, + updateDistribuidor, + deleteDistribuidor, +}; + +export default distribuidorService; \ No newline at end of file diff --git a/Frontend/src/services/empresaService.ts b/Frontend/src/services/Distribucion/empresaService.ts similarity index 83% rename from Frontend/src/services/empresaService.ts rename to Frontend/src/services/Distribucion/empresaService.ts index 0081843..9539674 100644 --- a/Frontend/src/services/empresaService.ts +++ b/Frontend/src/services/Distribucion/empresaService.ts @@ -1,7 +1,7 @@ -import apiClient from './apiClient'; -import type { EmpresaDto } from '../models/dtos/Empresas/EmpresaDto'; -import type { CreateEmpresaDto } from '../models/dtos/Empresas/CreateEmpresaDto'; -import type { UpdateEmpresaDto } from '../models/dtos/Empresas/UpdateEmpresaDto'; +import apiClient from '../apiClient'; +import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; +import type { CreateEmpresaDto } from '../../models/dtos/Distribucion/CreateEmpresaDto'; +import type { UpdateEmpresaDto } from '../../models/dtos/Distribucion/UpdateEmpresaDto'; const getAllEmpresas = async (nombreFilter?: string, detalleFilter?: string): Promise => { const params: Record = {}; diff --git a/Frontend/src/services/Distribucion/otroDestinoService.ts b/Frontend/src/services/Distribucion/otroDestinoService.ts new file mode 100644 index 0000000..63eea66 --- /dev/null +++ b/Frontend/src/services/Distribucion/otroDestinoService.ts @@ -0,0 +1,45 @@ +import apiClient from '../apiClient'; +import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto'; +import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto'; +import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto'; + +const getAllOtrosDestinos = async (nombreFilter?: string): Promise => { + const params: Record = {}; + if (nombreFilter) params.nombre = nombreFilter; + + // Llama a GET /api/otrosdestinos + const response = await apiClient.get('/otrosdestinos', { params }); + return response.data; +}; + +const getOtroDestinoById = async (id: number): Promise => { + // Llama a GET /api/otrosdestinos/{id} + const response = await apiClient.get(`/otrosdestinos/${id}`); + return response.data; +}; + +const createOtroDestino = async (data: CreateOtroDestinoDto): Promise => { + // Llama a POST /api/otrosdestinos + const response = await apiClient.post('/otrosdestinos', data); + return response.data; +}; + +const updateOtroDestino = async (id: number, data: UpdateOtroDestinoDto): Promise => { + // Llama a PUT /api/otrosdestinos/{id} + await apiClient.put(`/otrosdestinos/${id}`, data); +}; + +const deleteOtroDestino = async (id: number): Promise => { + // Llama a DELETE /api/otrosdestinos/{id} + await apiClient.delete(`/otrosdestinos/${id}`); +}; + +const otroDestinoService = { + getAllOtrosDestinos, + getOtroDestinoById, + createOtroDestino, + updateOtroDestino, + deleteOtroDestino, +}; + +export default otroDestinoService; \ No newline at end of file diff --git a/Frontend/src/services/Distribucion/precioService.ts b/Frontend/src/services/Distribucion/precioService.ts new file mode 100644 index 0000000..9cc665e --- /dev/null +++ b/Frontend/src/services/Distribucion/precioService.ts @@ -0,0 +1,40 @@ +import apiClient from '../apiClient'; +import type { PrecioDto } from '../../models/dtos/Distribucion/PrecioDto'; +import type { CreatePrecioDto } from '../../models/dtos/Distribucion/CreatePrecioDto'; +import type { UpdatePrecioDto } from '../../models/dtos/Distribucion/UpdatePrecioDto'; + +// El idPublicacion se pasa en la URL para estos endpoints +const getPreciosPorPublicacion = async (idPublicacion: number): Promise => { + const response = await apiClient.get(`/publicaciones/${idPublicacion}/precios`); + return response.data; +}; + +const getPrecioById = async (idPublicacion: number, idPrecio: number): Promise => { + const response = await apiClient.get(`/publicaciones/${idPublicacion}/precios/${idPrecio}`); + return response.data; +}; + +const createPrecio = async (idPublicacion: number, data: CreatePrecioDto): Promise => { + // Asegurarse que el DTO también contenga el idPublicacion si el backend lo espera en el cuerpo. + // En nuestro caso, el CreatePrecioDto ya tiene IdPublicacion. + const response = await apiClient.post(`/publicaciones/${idPublicacion}/precios`, data); + return response.data; +}; + +const updatePrecio = async (idPublicacion: number, idPrecio: number, data: UpdatePrecioDto): Promise => { + await apiClient.put(`/publicaciones/${idPublicacion}/precios/${idPrecio}`, data); +}; + +const deletePrecio = async (idPublicacion: number, idPrecio: number): Promise => { + await apiClient.delete(`/publicaciones/${idPublicacion}/precios/${idPrecio}`); +}; + +const precioService = { + getPreciosPorPublicacion, + getPrecioById, + createPrecio, + updatePrecio, + deletePrecio, +}; + +export default precioService; \ No newline at end of file diff --git a/Frontend/src/services/Distribucion/publicacionService.ts b/Frontend/src/services/Distribucion/publicacionService.ts new file mode 100644 index 0000000..32b9d85 --- /dev/null +++ b/Frontend/src/services/Distribucion/publicacionService.ts @@ -0,0 +1,46 @@ +import apiClient from '../apiClient'; +import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import type { CreatePublicacionDto } from '../../models/dtos/Distribucion/CreatePublicacionDto'; +import type { UpdatePublicacionDto } from '../../models/dtos/Distribucion/UpdatePublicacionDto'; + +const getAllPublicaciones = async ( + nombreFilter?: string, + idEmpresaFilter?: number, + soloHabilitadas?: boolean +): Promise => { + const params: Record = {}; + if (nombreFilter) params.nombre = nombreFilter; + if (idEmpresaFilter) params.idEmpresa = idEmpresaFilter; + if (soloHabilitadas !== undefined) params.soloHabilitadas = soloHabilitadas; + + const response = await apiClient.get('/publicaciones', { params }); + return response.data; +}; + +const getPublicacionById = async (id: number): Promise => { + const response = await apiClient.get(`/publicaciones/${id}`); + return response.data; +}; + +const createPublicacion = async (data: CreatePublicacionDto): Promise => { + const response = await apiClient.post('/publicaciones', data); + return response.data; +}; + +const updatePublicacion = async (id: number, data: UpdatePublicacionDto): Promise => { + await apiClient.put(`/publicaciones/${id}`, data); +}; + +const deletePublicacion = async (id: number): Promise => { + await apiClient.delete(`/publicaciones/${id}`); +}; + +const publicacionService = { + getAllPublicaciones, + getPublicacionById, + createPublicacion, + updatePublicacion, + deletePublicacion, +}; + +export default publicacionService; \ No newline at end of file diff --git a/Frontend/src/services/zonaService.ts b/Frontend/src/services/Distribucion/zonaService.ts similarity index 84% rename from Frontend/src/services/zonaService.ts rename to Frontend/src/services/Distribucion/zonaService.ts index 9837261..ecb6ee0 100644 --- a/Frontend/src/services/zonaService.ts +++ b/Frontend/src/services/Distribucion/zonaService.ts @@ -1,7 +1,7 @@ -import apiClient from './apiClient'; -import type { ZonaDto } from '../models/dtos/Zonas/ZonaDto'; // DTO para recibir listas -import type { CreateZonaDto } from '../models/dtos/Zonas/CreateZonaDto'; -import type { UpdateZonaDto } from '../models/dtos/Zonas/UpdateZonaDto'; +import apiClient from '../apiClient'; +import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // DTO para recibir listas +import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; +import type { UpdateZonaDto } from '../../models/dtos/Zonas/UpdateZonaDto'; const getAllZonas = async (nombreFilter?: string, descripcionFilter?: string): Promise => { const params: Record = {}; diff --git a/Frontend/src/services/estadoBobinaService.ts b/Frontend/src/services/Impresion/estadoBobinaService.ts similarity index 79% rename from Frontend/src/services/estadoBobinaService.ts rename to Frontend/src/services/Impresion/estadoBobinaService.ts index 3c257c5..715718a 100644 --- a/Frontend/src/services/estadoBobinaService.ts +++ b/Frontend/src/services/Impresion/estadoBobinaService.ts @@ -1,7 +1,7 @@ -import apiClient from './apiClient'; -import type { EstadoBobinaDto } from '../models/dtos/Impresion/EstadoBobinaDto'; -import type { CreateEstadoBobinaDto } from '../models/dtos/Impresion/CreateEstadoBobinaDto'; -import type { UpdateEstadoBobinaDto } from '../models/dtos/Impresion/UpdateEstadoBobinaDto'; +import apiClient from '../apiClient'; +import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; +import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto'; +import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto'; const getAllEstadosBobina = async (denominacionFilter?: string): Promise => { const params: Record = {}; diff --git a/Frontend/src/services/plantaService.ts b/Frontend/src/services/Impresion/plantaService.ts similarity index 83% rename from Frontend/src/services/plantaService.ts rename to Frontend/src/services/Impresion/plantaService.ts index 23f0636..d17a818 100644 --- a/Frontend/src/services/plantaService.ts +++ b/Frontend/src/services/Impresion/plantaService.ts @@ -1,7 +1,7 @@ -import apiClient from './apiClient'; -import type { PlantaDto } from '../models/dtos/Impresion/PlantaDto'; -import type { CreatePlantaDto } from '../models/dtos/Impresion/CreatePlantaDto'; -import type { UpdatePlantaDto } from '../models/dtos/Impresion/UpdatePlantaDto'; +import apiClient from '../apiClient'; +import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; +import type { CreatePlantaDto } from '../../models/dtos/Impresion/CreatePlantaDto'; +import type { UpdatePlantaDto } from '../../models/dtos/Impresion/UpdatePlantaDto'; const getAllPlantas = async (nombreFilter?: string, detalleFilter?: string): Promise => { const params: Record = {}; diff --git a/Frontend/src/services/tipoBobinaService.ts b/Frontend/src/services/Impresion/tipoBobinaService.ts similarity index 82% rename from Frontend/src/services/tipoBobinaService.ts rename to Frontend/src/services/Impresion/tipoBobinaService.ts index 3d61abc..00c4655 100644 --- a/Frontend/src/services/tipoBobinaService.ts +++ b/Frontend/src/services/Impresion/tipoBobinaService.ts @@ -1,7 +1,7 @@ -import apiClient from './apiClient'; -import type { TipoBobinaDto } from '../models/dtos/Impresion/TipoBobinaDto'; -import type { CreateTipoBobinaDto } from '../models/dtos/Impresion/CreateTipoBobinaDto'; -import type { UpdateTipoBobinaDto } from '../models/dtos/Impresion/UpdateTipoBobinaDto'; +import apiClient from '../apiClient'; +import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; +import type { CreateTipoBobinaDto } from '../../models/dtos/Impresion/CreateTipoBobinaDto'; +import type { UpdateTipoBobinaDto } from '../../models/dtos/Impresion/UpdateTipoBobinaDto'; const getAllTiposBobina = async (denominacionFilter?: string): Promise => { const params: Record = {}; diff --git a/Frontend/src/services/authService.ts b/Frontend/src/services/Usuarios/authService.ts similarity index 61% rename from Frontend/src/services/authService.ts rename to Frontend/src/services/Usuarios/authService.ts index be22bd7..34d45b8 100644 --- a/Frontend/src/services/authService.ts +++ b/Frontend/src/services/Usuarios/authService.ts @@ -1,7 +1,7 @@ -import apiClient from './apiClient'; -import type { LoginRequestDto } from '../models/dtos/LoginRequestDto'; -import type { LoginResponseDto } from '../models/dtos/LoginResponseDto'; -import type { ChangePasswordRequestDto } from '../models/dtos/ChangePasswordRequestDto'; // Importar DTO +import apiClient from '../apiClient'; +import type { LoginRequestDto } from '../../models/dtos/Usuarios/LoginRequestDto'; +import type { LoginResponseDto } from '../../models/dtos/Usuarios/LoginResponseDto'; +import type { ChangePasswordRequestDto } from '../../models/dtos/Usuarios/ChangePasswordRequestDto'; // Importar DTO const login = async (credentials: LoginRequestDto): Promise => { const response = await apiClient.post('/auth/login', credentials); diff --git a/Frontend/src/services/Usuarios/perfilService.ts b/Frontend/src/services/Usuarios/perfilService.ts new file mode 100644 index 0000000..bfcf022 --- /dev/null +++ b/Frontend/src/services/Usuarios/perfilService.ts @@ -0,0 +1,54 @@ +import apiClient from '../apiClient'; +import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto'; +import type { CreatePerfilDto } from '../../models/dtos/Usuarios/CreatePerfilDto'; +import type { UpdatePerfilDto } from '../../models/dtos/Usuarios/UpdatePerfilDto'; +import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto'; +import type { ActualizarPermisosPerfilRequestDto } from '../../models/dtos/Usuarios/ActualizarPermisosPerfilRequestDto'; + +const getAllPerfiles = async (nombreFilter?: string): Promise => { + const params: Record = {}; + if (nombreFilter) params.nombre = nombreFilter; // El backend espera 'nombre' + + const response = await apiClient.get('/perfiles', { params }); + return response.data; +}; + +const getPerfilById = async (id: number): Promise => { + const response = await apiClient.get(`/perfiles/${id}`); + return response.data; +}; + +const createPerfil = async (data: CreatePerfilDto): Promise => { + const response = await apiClient.post('/perfiles', data); + return response.data; +}; + +const updatePerfil = async (id: number, data: UpdatePerfilDto): Promise => { + await apiClient.put(`/perfiles/${id}`, data); +}; + +const deletePerfil = async (id: number): Promise => { + await apiClient.delete(`/perfiles/${id}`); +}; + +const getPermisosPorPerfil = async (idPerfil: number): Promise => { + const response = await apiClient.get(`/perfiles/${idPerfil}/permisos`); + return response.data; +}; + +const updatePermisosPorPerfil = async (idPerfil: number, data: ActualizarPermisosPerfilRequestDto): Promise => { + await apiClient.put(`/perfiles/${idPerfil}/permisos`, data); +}; + + +const perfilService = { + getAllPerfiles, + getPerfilById, + createPerfil, + updatePerfil, + deletePerfil, + getPermisosPorPerfil, + updatePermisosPorPerfil, +}; + +export default perfilService; \ No newline at end of file diff --git a/Frontend/src/services/Usuarios/permisoService.ts b/Frontend/src/services/Usuarios/permisoService.ts new file mode 100644 index 0000000..24bc1ee --- /dev/null +++ b/Frontend/src/services/Usuarios/permisoService.ts @@ -0,0 +1,42 @@ +import apiClient from '../apiClient'; +// Asegúrate que las rutas a los DTOs sean correctas +import type { PermisoDto } from '../../models/dtos/Usuarios/PermisoDto'; +import type { CreatePermisoDto } from '../../models/dtos/Usuarios/CreatePermisoDto.ts'; +import type { UpdatePermisoDto } from '../../models/dtos/Usuarios/UpdatePermisoDto'; + +const getAllPermisos = async (moduloFilter?: string, codAccFilter?: string): Promise => { + const params: Record = {}; + if (moduloFilter) params.modulo = moduloFilter; + if (codAccFilter) params.codAcc = codAccFilter; + + const response = await apiClient.get('/permisos', { params }); + return response.data; +}; + +const getPermisoById = async (id: number): Promise => { + const response = await apiClient.get(`/permisos/${id}`); + return response.data; +}; + +const createPermiso = async (data: CreatePermisoDto): Promise => { + const response = await apiClient.post('/permisos', data); + return response.data; +}; + +const updatePermiso = async (id: number, data: UpdatePermisoDto): Promise => { + await apiClient.put(`/permisos/${id}`, data); +}; + +const deletePermiso = async (id: number): Promise => { + await apiClient.delete(`/permisos/${id}`); +}; + +const permisoService = { + getAllPermisos, + getPermisoById, + createPermiso, + updatePermiso, + deletePermiso, +}; + +export default permisoService; \ No newline at end of file diff --git a/Frontend/src/services/Usuarios/usuarioService.ts b/Frontend/src/services/Usuarios/usuarioService.ts new file mode 100644 index 0000000..0835615 --- /dev/null +++ b/Frontend/src/services/Usuarios/usuarioService.ts @@ -0,0 +1,51 @@ +import apiClient from '../apiClient'; +import type { UsuarioDto } from '../../models/dtos/Usuarios/UsuarioDto'; +import type { CreateUsuarioRequestDto } from '../../models/dtos/Usuarios/CreateUsuarioRequestDto'; +import type { UpdateUsuarioRequestDto } from '../../models/dtos/Usuarios/UpdateUsuarioRequestDto'; +import type { SetPasswordRequestDto } from '../../models/dtos/Usuarios/SetPasswordRequestDto'; + +const getAllUsuarios = async (userFilter?: string, nombreFilter?: string): Promise => { + const params: Record = {}; + if (userFilter) params.user = userFilter; + if (nombreFilter) params.nombre = nombreFilter; + + const response = await apiClient.get('/usuarios', { params }); + return response.data; +}; + +const getUsuarioById = async (id: number): Promise => { + const response = await apiClient.get(`/usuarios/${id}`); + return response.data; +}; + +const createUsuario = async (data: CreateUsuarioRequestDto): Promise => { + const response = await apiClient.post('/usuarios', data); + return response.data; +}; + +const updateUsuario = async (id: number, data: UpdateUsuarioRequestDto): Promise => { + await apiClient.put(`/usuarios/${id}`, data); +}; + +const setPassword = async (id: number, data: SetPasswordRequestDto): Promise => { + await apiClient.post(`/usuarios/${id}/set-password`, data); +}; + +const toggleHabilitado = async (id: number, habilitar: boolean): Promise => { + // El backend espera un booleano simple en el cuerpo para este endpoint específico. + await apiClient.post(`/usuarios/${id}/toggle-habilitado`, habilitar, { + headers: { 'Content-Type': 'application/json' } // Asegurarse de que se envíe como JSON + }); +}; + + +const usuarioService = { + getAllUsuarios, + getUsuarioById, + createUsuario, + updateUsuario, + setPassword, + toggleHabilitado, +}; + +export default usuarioService; \ No newline at end of file diff --git a/tools/PasswordMigrationUtil/Program.cs b/tools/PasswordMigrationUtil/Program.cs index b6b2044..6615966 100644 --- a/tools/PasswordMigrationUtil/Program.cs +++ b/tools/PasswordMigrationUtil/Program.cs @@ -35,9 +35,8 @@ try Console.WriteLine("Conexión a base de datos establecida."); // Seleccionar usuarios que necesitan migración (ClaveSalt es NULL o vacía) - // ¡¡AJUSTA 'ClaveHash' AL NOMBRE DE LA COLUMNA QUE TIENE LA CLAVE EN TEXTO PLANO AHORA MISMO!! var usersToMigrateQuery = @" - SELECT Id, [User], ClaveHash AS PlainPassword -- Leer la clave plana de la columna correcta + SELECT Id, [User], Clave AS PlainPassword -- Leer la clave plana de la columna correcta FROM dbo.gral_Usuarios WHERE ClaveSalt IS NULL OR ClaveSalt = ''"; diff --git a/tools/PasswordMigrationUtil/obj/Debug/net9.0/PasswordMigrationUtil.AssemblyInfo.cs b/tools/PasswordMigrationUtil/obj/Debug/net9.0/PasswordMigrationUtil.AssemblyInfo.cs index 2248c75..492cc64 100644 --- a/tools/PasswordMigrationUtil/obj/Debug/net9.0/PasswordMigrationUtil.AssemblyInfo.cs +++ b/tools/PasswordMigrationUtil/obj/Debug/net9.0/PasswordMigrationUtil.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("PasswordMigrationUtil")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+9b1de95404118dad24e3e848866c48fce6e0c08e")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+daf84d27081c399bf1dbd5db88606c4b562cee46")] [assembly: System.Reflection.AssemblyProductAttribute("PasswordMigrationUtil")] [assembly: System.Reflection.AssemblyTitleAttribute("PasswordMigrationUtil")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/tools/PasswordMigrationUtil/obj/PasswordMigrationUtil.csproj.nuget.dgspec.json b/tools/PasswordMigrationUtil/obj/PasswordMigrationUtil.csproj.nuget.dgspec.json index 1846248..18ed0d4 100644 --- a/tools/PasswordMigrationUtil/obj/PasswordMigrationUtil.csproj.nuget.dgspec.json +++ b/tools/PasswordMigrationUtil/obj/PasswordMigrationUtil.csproj.nuget.dgspec.json @@ -14,7 +14,7 @@ "outputPath": "E:\\GestionIntegralWeb\\tools\\PasswordMigrationUtil\\obj\\", "projectStyle": "PackageReference", "fallbackFolders": [ - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" + "D:\\Microsoft\\VisualStudio\\Microsoft Visual Studio\\Shared\\NuGetPackages" ], "configFilePaths": [ "C:\\Users\\dmolinari\\AppData\\Roaming\\NuGet\\NuGet.Config", @@ -26,7 +26,6 @@ ], "sources": { "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, - "C:\\Program Files\\dotnet\\library-packs": {}, "https://api.nuget.org/v3/index.json": {} }, "frameworks": { @@ -45,7 +44,7 @@ "auditLevel": "low", "auditMode": "direct" }, - "SdkAnalysisLevel": "9.0.200" + "SdkAnalysisLevel": "9.0.300" }, "frameworks": { "net9.0": { @@ -88,7 +87,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.201/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.300/PortableRuntimeIdentifierGraph.json" } } } diff --git a/tools/PasswordMigrationUtil/obj/PasswordMigrationUtil.csproj.nuget.g.props b/tools/PasswordMigrationUtil/obj/PasswordMigrationUtil.csproj.nuget.g.props index 417de93..438e8fa 100644 --- a/tools/PasswordMigrationUtil/obj/PasswordMigrationUtil.csproj.nuget.g.props +++ b/tools/PasswordMigrationUtil/obj/PasswordMigrationUtil.csproj.nuget.g.props @@ -5,12 +5,12 @@ NuGet $(MSBuildThisFileDirectory)project.assets.json $(UserProfile)\.nuget\packages\ - C:\Users\dmolinari\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages + C:\Users\dmolinari\.nuget\packages\;D:\Microsoft\VisualStudio\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.13.1 + 6.14.0 - + \ No newline at end of file diff --git a/tools/PasswordMigrationUtil/obj/project.assets.json b/tools/PasswordMigrationUtil/obj/project.assets.json index 0f050b3..ef9940c 100644 --- a/tools/PasswordMigrationUtil/obj/project.assets.json +++ b/tools/PasswordMigrationUtil/obj/project.assets.json @@ -2076,7 +2076,7 @@ }, "packageFolders": { "C:\\Users\\dmolinari\\.nuget\\packages\\": {}, - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {} + "D:\\Microsoft\\VisualStudio\\Microsoft Visual Studio\\Shared\\NuGetPackages": {} }, "project": { "version": "1.0.0", @@ -2088,7 +2088,7 @@ "outputPath": "E:\\GestionIntegralWeb\\tools\\PasswordMigrationUtil\\obj\\", "projectStyle": "PackageReference", "fallbackFolders": [ - "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" + "D:\\Microsoft\\VisualStudio\\Microsoft Visual Studio\\Shared\\NuGetPackages" ], "configFilePaths": [ "C:\\Users\\dmolinari\\AppData\\Roaming\\NuGet\\NuGet.Config", @@ -2100,7 +2100,6 @@ ], "sources": { "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, - "C:\\Program Files\\dotnet\\library-packs": {}, "https://api.nuget.org/v3/index.json": {} }, "frameworks": { @@ -2119,7 +2118,7 @@ "auditLevel": "low", "auditMode": "direct" }, - "SdkAnalysisLevel": "9.0.200" + "SdkAnalysisLevel": "9.0.300" }, "frameworks": { "net9.0": { @@ -2162,7 +2161,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.201/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.300/PortableRuntimeIdentifierGraph.json" } } }