feat: Implementación CRUD Canillitas, Distribuidores y Precios de Publicación
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.
			
			
This commit is contained in:
		| @@ -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<CanillaService> _logger; | ||||
|  | ||||
|         public CanillaService( | ||||
|             ICanillaRepository canillaRepository, | ||||
|             IZonaRepository zonaRepository, | ||||
|             IEmpresaRepository empresaRepository, | ||||
|             DbConnectionFactory connectionFactory, | ||||
|             ILogger<CanillaService> 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<IEnumerable<CanillaDto>> 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<CanillaDto?> 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}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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<DistribuidorService> _logger; | ||||
|  | ||||
|         public DistribuidorService( | ||||
|             IDistribuidorRepository distribuidorRepository, | ||||
|             ISaldoRepository saldoRepository, | ||||
|             IEmpresaRepository empresaRepository, | ||||
|             IZonaRepository zonaRepository, | ||||
|             DbConnectionFactory connectionFactory, | ||||
|             ILogger<DistribuidorService> 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<IEnumerable<DistribuidorDto>> 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<DistribuidorDto?> 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}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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<IEnumerable<CanillaDto>> ObtenerTodosAsync(string? nomApeFilter, int? legajoFilter, bool? soloActivos); | ||||
|         Task<CanillaDto?> 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); | ||||
|     } | ||||
| } | ||||
| @@ -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<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter); | ||||
|         Task<DistribuidorDto?> 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); | ||||
|     } | ||||
| } | ||||
| @@ -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<IEnumerable<OtroDestinoDto>> ObtenerTodosAsync(string? nombreFilter); | ||||
|         Task<OtroDestinoDto?> 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); | ||||
|     } | ||||
| } | ||||
| @@ -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<IEnumerable<PrecioDto>> ObtenerPorPublicacionIdAsync(int idPublicacion); | ||||
|         Task<PrecioDto?> 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); | ||||
|     } | ||||
| } | ||||
| @@ -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<IEnumerable<PublicacionDto>> ObtenerTodasAsync(string? nombreFilter, int? idEmpresaFilter, bool? soloHabilitadas); | ||||
|         Task<PublicacionDto?> 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); | ||||
|     } | ||||
| } | ||||
| @@ -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<OtroDestinoService> _logger; | ||||
|  | ||||
|         public OtroDestinoService(IOtroDestinoRepository otroDestinoRepository, DbConnectionFactory connectionFactory, ILogger<OtroDestinoService> 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<IEnumerable<OtroDestinoDto>> ObtenerTodosAsync(string? nombreFilter) | ||||
|         { | ||||
|             var destinos = await _otroDestinoRepository.GetAllAsync(nombreFilter); | ||||
|             return destinos.Select(MapToDto); | ||||
|         } | ||||
|  | ||||
|         public async Task<OtroDestinoDto?> 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}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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<PrecioService> _logger; | ||||
|  | ||||
|         public PrecioService( | ||||
|             IPrecioRepository precioRepository, | ||||
|             IPublicacionRepository publicacionRepository, | ||||
|             DbConnectionFactory connectionFactory, | ||||
|             ILogger<PrecioService> 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<IEnumerable<PrecioDto>> ObtenerPorPublicacionIdAsync(int idPublicacion) | ||||
|         { | ||||
|             var precios = await _precioRepository.GetByPublicacionIdAsync(idPublicacion); | ||||
|             return precios.Select(MapToDto); | ||||
|         } | ||||
|         public async Task<PrecioDto?> 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}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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<PublicacionService> _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<PublicacionService> 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<IEnumerable<PublicacionDto>> 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<PublicacionDto?> 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}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|     { | ||||
| @@ -1,6 +1,6 @@ | ||||
| using GestionIntegral.Api.Dtos; | ||||
| 
 | ||||
| namespace GestionIntegral.Api.Services | ||||
| namespace GestionIntegral.Api.Services.Usuarios | ||||
| { | ||||
|     public interface IAuthService | ||||
|     { | ||||
| @@ -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<IEnumerable<PerfilDto>> ObtenerTodosAsync(string? nombreFilter); | ||||
|         Task<PerfilDto?> 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<IEnumerable<PermisoAsignadoDto>> ObtenerPermisosAsignadosAsync(int idPerfil); | ||||
|         Task<(bool Exito, string? Error)> ActualizarPermisosAsignadosAsync(int idPerfil, ActualizarPermisosPerfilRequestDto request, int idUsuarioModificador); | ||||
|     } | ||||
| } | ||||
| @@ -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<IEnumerable<PermisoDto>> ObtenerTodosAsync(string? moduloFilter, string? codAccFilter); | ||||
|         Task<PermisoDto?> 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); | ||||
|     } | ||||
| } | ||||
| @@ -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<IEnumerable<UsuarioDto>> ObtenerTodosAsync(string? userFilter, string? nombreFilter); | ||||
|         Task<UsuarioDto?> 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); | ||||
|  | ||||
|     } | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| 
 | ||||
| namespace GestionIntegral.Api.Services | ||||
| namespace GestionIntegral.Api.Services.Usuarios | ||||
| { | ||||
|     public class PasswordHasherService | ||||
|     { | ||||
							
								
								
									
										225
									
								
								Backend/GestionIntegral.Api/Services/Usuarios/PerfilService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								Backend/GestionIntegral.Api/Services/Usuarios/PerfilService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<PerfilService> _logger; | ||||
|         private readonly IPermisoRepository _permisoRepository; | ||||
|  | ||||
|         public PerfilService(IPerfilRepository perfilRepository, IPermisoRepository permisoRepository, DbConnectionFactory connectionFactory, ILogger<PerfilService> 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<IEnumerable<PerfilDto>> ObtenerTodosAsync(string? nombreFilter) | ||||
|         { | ||||
|             var perfiles = await _perfilRepository.GetAllAsync(nombreFilter); | ||||
|             return perfiles.Select(MapToDto); | ||||
|         } | ||||
|  | ||||
|         public async Task<PerfilDto?> 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<IEnumerable<PermisoAsignadoDto>> 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<int>(), 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}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										163
									
								
								Backend/GestionIntegral.Api/Services/Usuarios/PermisoService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								Backend/GestionIntegral.Api/Services/Usuarios/PermisoService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<PermisoService> _logger; | ||||
|  | ||||
|         public PermisoService(IPermisoRepository permisoRepository, DbConnectionFactory connectionFactory, ILogger<PermisoService> 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<IEnumerable<PermisoDto>> ObtenerTodosAsync(string? moduloFilter, string? codAccFilter) | ||||
|         { | ||||
|             var permisos = await _permisoRepository.GetAllAsync(moduloFilter, codAccFilter); | ||||
|             return permisos.Select(MapToDto); | ||||
|         } | ||||
|  | ||||
|         public async Task<PermisoDto?> 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}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										246
									
								
								Backend/GestionIntegral.Api/Services/Usuarios/UsuarioService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								Backend/GestionIntegral.Api/Services/Usuarios/UsuarioService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<UsuarioService> _logger; | ||||
|  | ||||
|         public UsuarioService( | ||||
|             IUsuarioRepository usuarioRepository, | ||||
|             IPerfilRepository perfilRepository, | ||||
|             PasswordHasherService passwordHasher, | ||||
|             DbConnectionFactory connectionFactory, | ||||
|             ILogger<UsuarioService> 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<IEnumerable<UsuarioDto>> 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<UsuarioDto?> 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}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user