Ya perdí el hilo de los cambios pero ahi van.
This commit is contained in:
		| @@ -0,0 +1,19 @@ | ||||
| using GestionIntegral.Api.Dtos.Contables; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Contables | ||||
| { | ||||
|     public interface INotaCreditoDebitoService | ||||
|     { | ||||
|         Task<IEnumerable<NotaCreditoDebitoDto>> ObtenerTodosAsync( | ||||
|             DateTime? fechaDesde, DateTime? fechaHasta, | ||||
|             string? destino, int? idDestino, int? idEmpresa, string? tipoNota); | ||||
|  | ||||
|         Task<NotaCreditoDebitoDto?> ObtenerPorIdAsync(int idNota); | ||||
|         Task<(NotaCreditoDebitoDto? Nota, string? Error)> CrearAsync(CreateNotaDto createDto, int idUsuario); | ||||
|         Task<(bool Exito, string? Error)> ActualizarAsync(int idNota, UpdateNotaDto updateDto, int idUsuario); | ||||
|         Task<(bool Exito, string? Error)> EliminarAsync(int idNota, int idUsuario); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,19 @@ | ||||
| using GestionIntegral.Api.Dtos.Contables; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Contables | ||||
| { | ||||
|     public interface IPagoDistribuidorService | ||||
|     { | ||||
|         Task<IEnumerable<PagoDistribuidorDto>> ObtenerTodosAsync( | ||||
|             DateTime? fechaDesde, DateTime? fechaHasta, | ||||
|             int? idDistribuidor, int? idEmpresa, string? tipoMovimiento); | ||||
|  | ||||
|         Task<PagoDistribuidorDto?> ObtenerPorIdAsync(int idPago); | ||||
|         Task<(PagoDistribuidorDto? Pago, string? Error)> CrearAsync(CreatePagoDistribuidorDto createDto, int idUsuario); | ||||
|         Task<(bool Exito, string? Error)> ActualizarAsync(int idPago, UpdatePagoDistribuidorDto updateDto, int idUsuario); | ||||
|         Task<(bool Exito, string? Error)> EliminarAsync(int idPago, int idUsuario); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,226 @@ | ||||
| using GestionIntegral.Api.Data; | ||||
| using GestionIntegral.Api.Data.Repositories.Contables; | ||||
| using GestionIntegral.Api.Data.Repositories.Distribucion; | ||||
| using GestionIntegral.Api.Dtos.Contables; | ||||
| using GestionIntegral.Api.Models.Contables; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Data; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Contables | ||||
| { | ||||
|     public class NotaCreditoDebitoService : INotaCreditoDebitoService | ||||
|     { | ||||
|         private readonly INotaCreditoDebitoRepository _notaRepo; | ||||
|         private readonly IDistribuidorRepository _distribuidorRepo; | ||||
|         private readonly ICanillaRepository _canillaRepo; | ||||
|         private readonly IEmpresaRepository _empresaRepo; | ||||
|         private readonly ISaldoRepository _saldoRepo; | ||||
|         private readonly DbConnectionFactory _connectionFactory; | ||||
|         private readonly ILogger<NotaCreditoDebitoService> _logger; | ||||
|  | ||||
|         public NotaCreditoDebitoService( | ||||
|             INotaCreditoDebitoRepository notaRepo, | ||||
|             IDistribuidorRepository distribuidorRepo, | ||||
|             ICanillaRepository canillaRepo, | ||||
|             IEmpresaRepository empresaRepo, | ||||
|             ISaldoRepository saldoRepo, | ||||
|             DbConnectionFactory connectionFactory, | ||||
|             ILogger<NotaCreditoDebitoService> logger) | ||||
|         { | ||||
|             _notaRepo = notaRepo; | ||||
|             _distribuidorRepo = distribuidorRepo; | ||||
|             _canillaRepo = canillaRepo; | ||||
|             _empresaRepo = empresaRepo; | ||||
|             _saldoRepo = saldoRepo; | ||||
|             _connectionFactory = connectionFactory; | ||||
|             _logger = logger; | ||||
|         } | ||||
|  | ||||
|         private async Task<NotaCreditoDebitoDto> MapToDto(NotaCreditoDebito nota) | ||||
|         { | ||||
|             if (nota == null) return null!; | ||||
|  | ||||
|             string nombreDestinatario = "N/A"; | ||||
|             if (nota.Destino == "Distribuidores") | ||||
|             { | ||||
|                 var distData = await _distribuidorRepo.GetByIdAsync(nota.IdDestino); | ||||
|                 nombreDestinatario = distData.Distribuidor?.Nombre ?? "Distribuidor Desconocido"; | ||||
|             } | ||||
|             else if (nota.Destino == "Canillas") | ||||
|             { | ||||
|                 var canData = await _canillaRepo.GetByIdAsync(nota.IdDestino); // Asumiendo que GetByIdAsync devuelve una tupla | ||||
|                 nombreDestinatario = canData.Canilla?.NomApe ?? "Canillita Desconocido"; | ||||
|             } | ||||
|              | ||||
|             var empresa = await _empresaRepo.GetByIdAsync(nota.IdEmpresa); | ||||
|  | ||||
|             return new NotaCreditoDebitoDto | ||||
|             { | ||||
|                 IdNota = nota.IdNota, | ||||
|                 Destino = nota.Destino, | ||||
|                 IdDestino = nota.IdDestino, | ||||
|                 NombreDestinatario = nombreDestinatario, | ||||
|                 Referencia = nota.Referencia, | ||||
|                 Tipo = nota.Tipo, | ||||
|                 Fecha = nota.Fecha.ToString("yyyy-MM-dd"), | ||||
|                 Monto = nota.Monto, | ||||
|                 Observaciones = nota.Observaciones, | ||||
|                 IdEmpresa = nota.IdEmpresa, | ||||
|                 NombreEmpresa = empresa?.Nombre ?? "Empresa Desconocida" | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<NotaCreditoDebitoDto>> ObtenerTodosAsync( | ||||
|             DateTime? fechaDesde, DateTime? fechaHasta, | ||||
|             string? destino, int? idDestino, int? idEmpresa, string? tipoNota) | ||||
|         { | ||||
|             var notas = await _notaRepo.GetAllAsync(fechaDesde, fechaHasta, destino, idDestino, idEmpresa, tipoNota); | ||||
|             var dtos = new List<NotaCreditoDebitoDto>(); | ||||
|             foreach (var nota in notas) | ||||
|             { | ||||
|                 dtos.Add(await MapToDto(nota)); | ||||
|             } | ||||
|             return dtos; | ||||
|         } | ||||
|  | ||||
|         public async Task<NotaCreditoDebitoDto?> ObtenerPorIdAsync(int idNota) | ||||
|         { | ||||
|             var nota = await _notaRepo.GetByIdAsync(idNota); | ||||
|             return nota == null ? null : await MapToDto(nota); | ||||
|         } | ||||
|  | ||||
|         public async Task<(NotaCreditoDebitoDto? Nota, string? Error)> CrearAsync(CreateNotaDto createDto, int idUsuario) | ||||
|         { | ||||
|             // Validar Destinatario | ||||
|             if (createDto.Destino == "Distribuidores") | ||||
|             { | ||||
|                 if (await _distribuidorRepo.GetByIdSimpleAsync(createDto.IdDestino) == null) | ||||
|                     return (null, "El distribuidor especificado no existe."); | ||||
|             } | ||||
|             else if (createDto.Destino == "Canillas") | ||||
|             { | ||||
|                 if (await _canillaRepo.GetByIdSimpleAsync(createDto.IdDestino) == null) // Asumiendo GetByIdSimpleAsync en ICanillaRepository | ||||
|                     return (null, "El canillita especificado no existe."); | ||||
|             } | ||||
|             else { return (null, "Tipo de destino inválido."); } | ||||
|  | ||||
|             if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null) | ||||
|                 return (null, "La empresa especificada no existe."); | ||||
|  | ||||
|             var nuevaNota = new NotaCreditoDebito | ||||
|             { | ||||
|                 Destino = createDto.Destino, | ||||
|                 IdDestino = createDto.IdDestino, | ||||
|                 Referencia = createDto.Referencia, | ||||
|                 Tipo = createDto.Tipo, | ||||
|                 Fecha = createDto.Fecha.Date, | ||||
|                 Monto = createDto.Monto, | ||||
|                 Observaciones = createDto.Observaciones, | ||||
|                 IdEmpresa = createDto.IdEmpresa | ||||
|             }; | ||||
|  | ||||
|             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 notaCreada = await _notaRepo.CreateAsync(nuevaNota, idUsuario, transaction); | ||||
|                 if (notaCreada == null) throw new DataException("Error al registrar la nota."); | ||||
|  | ||||
|                 // Afectar Saldo | ||||
|                 // Nota de Crédito: Disminuye la deuda del destinatario (monto positivo para el servicio de saldo) | ||||
|                 // Nota de Débito: Aumenta la deuda del destinatario (monto negativo para el servicio de saldo) | ||||
|                 decimal montoAjusteSaldo = createDto.Tipo == "Credito" ? createDto.Monto : -createDto.Monto; | ||||
|  | ||||
|                 bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaCreada.Destino, notaCreada.IdDestino, notaCreada.IdEmpresa, montoAjusteSaldo, transaction); | ||||
|                 if (!saldoActualizado) throw new DataException($"Error al actualizar el saldo para {notaCreada.Destino} ID {notaCreada.IdDestino}."); | ||||
|  | ||||
|                 transaction.Commit(); | ||||
|                 _logger.LogInformation("NotaC/D ID {Id} creada y saldo afectado por Usuario ID {UserId}.", notaCreada.IdNota, idUsuario); | ||||
|                 return (await MapToDto(notaCreada), null); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 try { transaction.Rollback(); } catch { } | ||||
|                 _logger.LogError(ex, "Error CrearAsync NotaCreditoDebito."); | ||||
|                 return (null, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> ActualizarAsync(int idNota, UpdateNotaDto 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 notaExistente = await _notaRepo.GetByIdAsync(idNota); | ||||
|                 if (notaExistente == null) return (false, "Nota no encontrada."); | ||||
|  | ||||
|                 // Calcular diferencia de monto para ajustar saldo | ||||
|                 decimal montoOriginal = notaExistente.Tipo == "Credito" ? notaExistente.Monto : -notaExistente.Monto; | ||||
|                 decimal montoNuevo = notaExistente.Tipo == "Credito" ? updateDto.Monto : -updateDto.Monto; // Tipo no cambia | ||||
|                 decimal diferenciaAjusteSaldo = montoNuevo - montoOriginal; | ||||
|  | ||||
|                 notaExistente.Monto = updateDto.Monto; | ||||
|                 notaExistente.Observaciones = updateDto.Observaciones; | ||||
|  | ||||
|                 var actualizado = await _notaRepo.UpdateAsync(notaExistente, idUsuario, transaction); | ||||
|                 if (!actualizado) throw new DataException("Error al actualizar la nota."); | ||||
|  | ||||
|                 if (diferenciaAjusteSaldo != 0) | ||||
|                 { | ||||
|                     bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, diferenciaAjusteSaldo, transaction); | ||||
|                     if (!saldoActualizado) throw new DataException("Error al ajustar el saldo tras la actualización de la nota."); | ||||
|                 } | ||||
|  | ||||
|                 transaction.Commit(); | ||||
|                 _logger.LogInformation("NotaC/D ID {Id} actualizada por Usuario ID {UserId}.", idNota, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Nota no encontrada."); } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 try { transaction.Rollback(); } catch { } | ||||
|                 _logger.LogError(ex, "Error ActualizarAsync NotaC/D ID: {Id}", idNota); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> EliminarAsync(int idNota, 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 notaExistente = await _notaRepo.GetByIdAsync(idNota); | ||||
|                 if (notaExistente == null) return (false, "Nota no encontrada."); | ||||
|  | ||||
|                 // Revertir el efecto en el saldo | ||||
|                 decimal montoReversion = notaExistente.Tipo == "Credito" ? -notaExistente.Monto : notaExistente.Monto; | ||||
|  | ||||
|                 var eliminado = await _notaRepo.DeleteAsync(idNota, idUsuario, transaction); | ||||
|                 if (!eliminado) throw new DataException("Error al eliminar la nota."); | ||||
|  | ||||
|                 bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, montoReversion, transaction); | ||||
|                 if (!saldoActualizado) throw new DataException("Error al revertir el saldo tras la eliminación de la nota."); | ||||
|  | ||||
|                 transaction.Commit(); | ||||
|                 _logger.LogInformation("NotaC/D ID {Id} eliminada y saldo revertido por Usuario ID {UserId}.", idNota, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Nota no encontrada."); } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 try { transaction.Rollback(); } catch { } | ||||
|                 _logger.LogError(ex, "Error EliminarAsync NotaC/D ID: {Id}", idNota); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,218 @@ | ||||
| using GestionIntegral.Api.Data; | ||||
| using GestionIntegral.Api.Data.Repositories.Contables; | ||||
| using GestionIntegral.Api.Data.Repositories.Distribucion; | ||||
| using GestionIntegral.Api.Dtos.Contables; | ||||
| using GestionIntegral.Api.Models.Contables; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Data; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Contables | ||||
| { | ||||
|     public class PagoDistribuidorService : IPagoDistribuidorService | ||||
|     { | ||||
|         private readonly IPagoDistribuidorRepository _pagoRepo; | ||||
|         private readonly IDistribuidorRepository _distribuidorRepo; | ||||
|         private readonly ITipoPagoRepository _tipoPagoRepo; | ||||
|         private readonly IEmpresaRepository _empresaRepo; | ||||
|         private readonly ISaldoRepository _saldoRepo; | ||||
|         private readonly DbConnectionFactory _connectionFactory; | ||||
|         private readonly ILogger<PagoDistribuidorService> _logger; | ||||
|  | ||||
|         public PagoDistribuidorService( | ||||
|             IPagoDistribuidorRepository pagoRepo, | ||||
|             IDistribuidorRepository distribuidorRepo, | ||||
|             ITipoPagoRepository tipoPagoRepo, | ||||
|             IEmpresaRepository empresaRepo, | ||||
|             ISaldoRepository saldoRepo, | ||||
|             DbConnectionFactory connectionFactory, | ||||
|             ILogger<PagoDistribuidorService> logger) | ||||
|         { | ||||
|             _pagoRepo = pagoRepo; | ||||
|             _distribuidorRepo = distribuidorRepo; | ||||
|             _tipoPagoRepo = tipoPagoRepo; | ||||
|             _empresaRepo = empresaRepo; | ||||
|             _saldoRepo = saldoRepo; | ||||
|             _connectionFactory = connectionFactory; | ||||
|             _logger = logger; | ||||
|         } | ||||
|  | ||||
|         private async Task<PagoDistribuidorDto> MapToDto(PagoDistribuidor pago) | ||||
|         { | ||||
|             if (pago == null) return null!; | ||||
|  | ||||
|             var distribuidorData = await _distribuidorRepo.GetByIdAsync(pago.IdDistribuidor); | ||||
|             var tipoPago = await _tipoPagoRepo.GetByIdAsync(pago.IdTipoPago); | ||||
|             var empresa = await _empresaRepo.GetByIdAsync(pago.IdEmpresa); | ||||
|  | ||||
|             return new PagoDistribuidorDto | ||||
|             { | ||||
|                 IdPago = pago.IdPago, | ||||
|                 IdDistribuidor = pago.IdDistribuidor, | ||||
|                 NombreDistribuidor = distribuidorData.Distribuidor?.Nombre ?? "N/A", | ||||
|                 Fecha = pago.Fecha.ToString("yyyy-MM-dd"), | ||||
|                 TipoMovimiento = pago.TipoMovimiento, | ||||
|                 Recibo = pago.Recibo, | ||||
|                 Monto = pago.Monto, | ||||
|                 IdTipoPago = pago.IdTipoPago, | ||||
|                 NombreTipoPago = tipoPago?.Nombre ?? "N/A", | ||||
|                 Detalle = pago.Detalle, | ||||
|                 IdEmpresa = pago.IdEmpresa, | ||||
|                 NombreEmpresa = empresa?.Nombre ?? "N/A" | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<PagoDistribuidorDto>> ObtenerTodosAsync( | ||||
|             DateTime? fechaDesde, DateTime? fechaHasta, | ||||
|             int? idDistribuidor, int? idEmpresa, string? tipoMovimiento) | ||||
|         { | ||||
|             var pagos = await _pagoRepo.GetAllAsync(fechaDesde, fechaHasta, idDistribuidor, idEmpresa, tipoMovimiento); | ||||
|             var dtos = new List<PagoDistribuidorDto>(); | ||||
|             foreach (var pago in pagos) | ||||
|             { | ||||
|                 dtos.Add(await MapToDto(pago)); | ||||
|             } | ||||
|             return dtos; | ||||
|         } | ||||
|  | ||||
|         public async Task<PagoDistribuidorDto?> ObtenerPorIdAsync(int idPago) | ||||
|         { | ||||
|             var pago = await _pagoRepo.GetByIdAsync(idPago); | ||||
|             return pago == null ? null : await MapToDto(pago); | ||||
|         } | ||||
|  | ||||
|         public async Task<(PagoDistribuidorDto? Pago, string? Error)> CrearAsync(CreatePagoDistribuidorDto createDto, int idUsuario) | ||||
|         { | ||||
|             if (await _distribuidorRepo.GetByIdSimpleAsync(createDto.IdDistribuidor) == null) | ||||
|                 return (null, "Distribuidor no válido."); | ||||
|             if (await _tipoPagoRepo.GetByIdAsync(createDto.IdTipoPago) == null) | ||||
|                 return (null, "Tipo de pago no válido."); | ||||
|             if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null) | ||||
|                 return (null, "Empresa no válida."); | ||||
|             if (await _pagoRepo.ExistsByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento)) | ||||
|                  return (null, $"Ya existe un pago '{createDto.TipoMovimiento}' con el número de recibo '{createDto.Recibo}'."); | ||||
|  | ||||
|  | ||||
|             var nuevoPago = new PagoDistribuidor | ||||
|             { | ||||
|                 IdDistribuidor = createDto.IdDistribuidor, | ||||
|                 Fecha = createDto.Fecha.Date, | ||||
|                 TipoMovimiento = createDto.TipoMovimiento, | ||||
|                 Recibo = createDto.Recibo, | ||||
|                 Monto = createDto.Monto, | ||||
|                 IdTipoPago = createDto.IdTipoPago, | ||||
|                 Detalle = createDto.Detalle, | ||||
|                 IdEmpresa = createDto.IdEmpresa | ||||
|             }; | ||||
|  | ||||
|             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 pagoCreado = await _pagoRepo.CreateAsync(nuevoPago, idUsuario, transaction); | ||||
|                 if (pagoCreado == null) throw new DataException("Error al registrar el pago."); | ||||
|  | ||||
|                 // Afectar Saldo | ||||
|                 // Si TipoMovimiento es "Recibido", el monto DISMINUYE la deuda del distribuidor (monto positivo para el servicio de saldo). | ||||
|                 // Si TipoMovimiento es "Realizado" (empresa paga a distribuidor), el monto AUMENTA la deuda (monto negativo para el servicio de saldo). | ||||
|                 decimal montoAjusteSaldo = createDto.TipoMovimiento == "Recibido" ? createDto.Monto : -createDto.Monto; | ||||
|  | ||||
|                 bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoCreado.IdDistribuidor, pagoCreado.IdEmpresa, montoAjusteSaldo, transaction); | ||||
|                 if (!saldoActualizado) throw new DataException("Error al actualizar el saldo del distribuidor."); | ||||
|  | ||||
|                 transaction.Commit(); | ||||
|                 _logger.LogInformation("PagoDistribuidor ID {Id} creado y saldo afectado por Usuario ID {UserId}.", pagoCreado.IdPago, idUsuario); | ||||
|                 return (await MapToDto(pagoCreado), null); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 try { transaction.Rollback(); } catch { } | ||||
|                 _logger.LogError(ex, "Error CrearAsync PagoDistribuidor."); | ||||
|                 return (null, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> ActualizarAsync(int idPago, UpdatePagoDistribuidorDto 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 pagoExistente = await _pagoRepo.GetByIdAsync(idPago); | ||||
|                 if (pagoExistente == null) return (false, "Pago no encontrado."); | ||||
|  | ||||
|                 if (await _tipoPagoRepo.GetByIdAsync(updateDto.IdTipoPago) == null) | ||||
|                     return (false, "Tipo de pago no válido."); | ||||
|  | ||||
|                 // Calcular la diferencia de monto para ajustar el saldo | ||||
|                 decimal montoOriginal = pagoExistente.TipoMovimiento == "Recibido" ? pagoExistente.Monto : -pagoExistente.Monto; | ||||
|                 decimal montoNuevo = pagoExistente.TipoMovimiento == "Recibido" ? updateDto.Monto : -updateDto.Monto; | ||||
|                 decimal diferenciaAjusteSaldo = montoNuevo - montoOriginal; | ||||
|  | ||||
|                 // Actualizar campos permitidos | ||||
|                 pagoExistente.Monto = updateDto.Monto; | ||||
|                 pagoExistente.IdTipoPago = updateDto.IdTipoPago; | ||||
|                 pagoExistente.Detalle = updateDto.Detalle; | ||||
|  | ||||
|                 var actualizado = await _pagoRepo.UpdateAsync(pagoExistente, idUsuario, transaction); | ||||
|                 if (!actualizado) throw new DataException("Error al actualizar el pago."); | ||||
|  | ||||
|                 if (diferenciaAjusteSaldo != 0) | ||||
|                 { | ||||
|                     bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoExistente.IdDistribuidor, pagoExistente.IdEmpresa, diferenciaAjusteSaldo, transaction); | ||||
|                     if (!saldoActualizado) throw new DataException("Error al ajustar el saldo del distribuidor tras la actualización del pago."); | ||||
|                 } | ||||
|  | ||||
|                 transaction.Commit(); | ||||
|                 _logger.LogInformation("PagoDistribuidor ID {Id} actualizado por Usuario ID {UserId}.", idPago, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Pago no encontrado."); } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 try { transaction.Rollback(); } catch { } | ||||
|                 _logger.LogError(ex, "Error ActualizarAsync PagoDistribuidor ID: {Id}", idPago); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> EliminarAsync(int idPago, 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 pagoExistente = await _pagoRepo.GetByIdAsync(idPago); | ||||
|                 if (pagoExistente == null) return (false, "Pago no encontrado."); | ||||
|  | ||||
|                 // Revertir el efecto en el saldo | ||||
|                 // Si fue "Recibido", el saldo disminuyó (montoAjusteSaldo fue +Monto). Al eliminar, revertimos sumando -Monto (o restando +Monto). | ||||
|                 // Si fue "Realizado", el saldo aumentó (montoAjusteSaldo fue -Monto). Al eliminar, revertimos sumando +Monto (o restando -Monto). | ||||
|                 decimal montoReversion = pagoExistente.TipoMovimiento == "Recibido" ? -pagoExistente.Monto : pagoExistente.Monto; | ||||
|  | ||||
|                 var eliminado = await _pagoRepo.DeleteAsync(idPago, idUsuario, transaction); | ||||
|                 if (!eliminado) throw new DataException("Error al eliminar el pago."); | ||||
|  | ||||
|                 bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync("Distribuidores", pagoExistente.IdDistribuidor, pagoExistente.IdEmpresa, montoReversion, transaction); | ||||
|                 if (!saldoActualizado) throw new DataException("Error al revertir el saldo del distribuidor tras la eliminación del pago."); | ||||
|  | ||||
|                 transaction.Commit(); | ||||
|                 _logger.LogInformation("PagoDistribuidor ID {Id} eliminado por Usuario ID {UserId}.", idPago, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Pago no encontrado."); } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 try { transaction.Rollback(); } catch { } | ||||
|                 _logger.LogError(ex, "Error EliminarAsync PagoDistribuidor ID: {Id}", idPago); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,171 @@ | ||||
| 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.Linq; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Distribucion | ||||
| { | ||||
|     public class ControlDevolucionesService : IControlDevolucionesService | ||||
|     { | ||||
|         private readonly IControlDevolucionesRepository _controlDevRepo; | ||||
|         private readonly IEmpresaRepository _empresaRepository; // Para validar IdEmpresa y obtener nombre | ||||
|         private readonly DbConnectionFactory _connectionFactory; | ||||
|         private readonly ILogger<ControlDevolucionesService> _logger; | ||||
|  | ||||
|         public ControlDevolucionesService( | ||||
|             IControlDevolucionesRepository controlDevRepo, | ||||
|             IEmpresaRepository empresaRepository, | ||||
|             DbConnectionFactory connectionFactory, | ||||
|             ILogger<ControlDevolucionesService> logger) | ||||
|         { | ||||
|             _controlDevRepo = controlDevRepo; | ||||
|             _empresaRepository = empresaRepository; | ||||
|             _connectionFactory = connectionFactory; | ||||
|             _logger = logger; | ||||
|         } | ||||
|  | ||||
|         private async Task<ControlDevolucionesDto> MapToDto(ControlDevoluciones control) | ||||
|         { | ||||
|             if (control == null) return null!; | ||||
|             var empresa = await _empresaRepository.GetByIdAsync(control.IdEmpresa); | ||||
|             return new ControlDevolucionesDto | ||||
|             { | ||||
|                 IdControl = control.IdControl, | ||||
|                 IdEmpresa = control.IdEmpresa, | ||||
|                 NombreEmpresa = empresa?.Nombre ?? "Empresa Desconocida", | ||||
|                 Fecha = control.Fecha.ToString("yyyy-MM-dd"), | ||||
|                 Entrada = control.Entrada, | ||||
|                 Sobrantes = control.Sobrantes, | ||||
|                 Detalle = control.Detalle, | ||||
|                 SinCargo = control.SinCargo | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<ControlDevolucionesDto>> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idEmpresa) | ||||
|         { | ||||
|             var controles = await _controlDevRepo.GetAllAsync(fechaDesde, fechaHasta, idEmpresa); | ||||
|             var dtos = new List<ControlDevolucionesDto>(); | ||||
|             foreach (var control in controles) | ||||
|             { | ||||
|                 dtos.Add(await MapToDto(control)); | ||||
|             } | ||||
|             return dtos; | ||||
|         } | ||||
|  | ||||
|         public async Task<ControlDevolucionesDto?> ObtenerPorIdAsync(int idControl) | ||||
|         { | ||||
|             var control = await _controlDevRepo.GetByIdAsync(idControl); | ||||
|             return control == null ? null : await MapToDto(control); | ||||
|         } | ||||
|  | ||||
|         public async Task<(ControlDevolucionesDto? Control, string? Error)> CrearAsync(CreateControlDevolucionesDto createDto, int idUsuario) | ||||
|         { | ||||
|             var empresa = await _empresaRepository.GetByIdAsync(createDto.IdEmpresa); | ||||
|             if (empresa == null) | ||||
|                 return (null, "La empresa especificada no existe."); | ||||
|  | ||||
|             // Validar que no exista ya un control para esa empresa y fecha | ||||
|             if (await _controlDevRepo.GetByEmpresaAndFechaAsync(createDto.IdEmpresa, createDto.Fecha.Date) != null) | ||||
|             { | ||||
|                 return (null, $"Ya existe un control de devoluciones para la empresa '{empresa.Nombre}' en la fecha {createDto.Fecha:dd/MM/yyyy}."); | ||||
|             } | ||||
|  | ||||
|             var nuevoControl = new ControlDevoluciones | ||||
|             { | ||||
|                 IdEmpresa = createDto.IdEmpresa, | ||||
|                 Fecha = createDto.Fecha.Date, | ||||
|                 Entrada = createDto.Entrada, | ||||
|                 Sobrantes = createDto.Sobrantes, | ||||
|                 Detalle = createDto.Detalle, | ||||
|                 SinCargo = createDto.SinCargo | ||||
|             }; | ||||
|  | ||||
|             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 controlCreado = await _controlDevRepo.CreateAsync(nuevoControl, idUsuario, transaction); | ||||
|                 if (controlCreado == null) throw new DataException("Error al registrar el control de devoluciones."); | ||||
|  | ||||
|                 transaction.Commit(); | ||||
|                 _logger.LogInformation("ControlDevoluciones ID {Id} creado por Usuario ID {UserId}.", controlCreado.IdControl, idUsuario); | ||||
|                 return (await MapToDto(controlCreado), null); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 try { transaction.Rollback(); } catch { } | ||||
|                 _logger.LogError(ex, "Error CrearAsync ControlDevoluciones para Empresa ID {IdEmpresa}, Fecha {Fecha}", createDto.IdEmpresa, createDto.Fecha); | ||||
|                 return (null, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> ActualizarAsync(int idControl, UpdateControlDevolucionesDto 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 controlExistente = await _controlDevRepo.GetByIdAsync(idControl); // Obtener dentro de TX | ||||
|                 if (controlExistente == null) return (false, "Control de devoluciones no encontrado."); | ||||
|  | ||||
|                 // IdEmpresa y Fecha no se modifican | ||||
|                 controlExistente.Entrada = updateDto.Entrada; | ||||
|                 controlExistente.Sobrantes = updateDto.Sobrantes; | ||||
|                 controlExistente.Detalle = updateDto.Detalle; | ||||
|                 controlExistente.SinCargo = updateDto.SinCargo; | ||||
|  | ||||
|                 var actualizado = await _controlDevRepo.UpdateAsync(controlExistente, idUsuario, transaction); | ||||
|                 if (!actualizado) throw new DataException("Error al actualizar el control de devoluciones."); | ||||
|  | ||||
|                 transaction.Commit(); | ||||
|                 _logger.LogInformation("ControlDevoluciones ID {Id} actualizado por Usuario ID {UserId}.", idControl, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Registro no encontrado."); } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 try { transaction.Rollback(); } catch { } | ||||
|                 _logger.LogError(ex, "Error ActualizarAsync ControlDevoluciones ID: {Id}", idControl); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> EliminarAsync(int idControl, 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 controlExistente = await _controlDevRepo.GetByIdAsync(idControl); // Obtener dentro de TX | ||||
|                 if (controlExistente == null) return (false, "Control de devoluciones no encontrado."); | ||||
|  | ||||
|                 // Aquí no hay dependencias directas que afecten saldos, es un registro informativo. | ||||
|                 // Se podría verificar si está "en uso" si alguna lógica de reporte o cierre depende de él, | ||||
|                 // pero por ahora, la eliminación es directa. | ||||
|  | ||||
|                 var eliminado = await _controlDevRepo.DeleteAsync(idControl, idUsuario, transaction); | ||||
|                 if (!eliminado) throw new DataException("Error al eliminar el control de devoluciones."); | ||||
|  | ||||
|                 transaction.Commit(); | ||||
|                 _logger.LogInformation("ControlDevoluciones ID {Id} eliminado por Usuario ID {UserId}.", idControl, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Registro no encontrado."); } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 try { transaction.Rollback(); } catch { } | ||||
|                 _logger.LogError(ex, "Error EliminarAsync ControlDevoluciones ID: {Id}", idControl); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,481 @@ | ||||
| using GestionIntegral.Api.Data; | ||||
| using GestionIntegral.Api.Data.Repositories.Distribucion; | ||||
| using GestionIntegral.Api.Data.Repositories.Usuarios; | ||||
| 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; | ||||
| using System.Security.Claims; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Distribucion | ||||
| { | ||||
|     public class EntradaSalidaCanillaService : IEntradaSalidaCanillaService | ||||
|     { | ||||
|         private readonly IEntradaSalidaCanillaRepository _esCanillaRepository; | ||||
|         private readonly IPublicacionRepository _publicacionRepository; | ||||
|         private readonly ICanillaRepository _canillaRepository; | ||||
|         private readonly IPrecioRepository _precioRepository; | ||||
|         private readonly IRecargoZonaRepository _recargoZonaRepository; | ||||
|         private readonly IPorcMonCanillaRepository _porcMonCanillaRepository; | ||||
|         private readonly IUsuarioRepository _usuarioRepository; | ||||
|         private readonly DbConnectionFactory _connectionFactory; | ||||
|         private readonly ILogger<EntradaSalidaCanillaService> _logger; | ||||
|  | ||||
|         public EntradaSalidaCanillaService( | ||||
|             IEntradaSalidaCanillaRepository esCanillaRepository, | ||||
|             IPublicacionRepository publicacionRepository, | ||||
|             ICanillaRepository canillaRepository, | ||||
|             IPrecioRepository precioRepository, | ||||
|             IRecargoZonaRepository recargoZonaRepository, | ||||
|             IPorcMonCanillaRepository porcMonCanillaRepository, | ||||
|             IUsuarioRepository usuarioRepository, | ||||
|             DbConnectionFactory connectionFactory, | ||||
|             ILogger<EntradaSalidaCanillaService> logger) | ||||
|         { | ||||
|             _esCanillaRepository = esCanillaRepository; | ||||
|             _publicacionRepository = publicacionRepository; | ||||
|             _canillaRepository = canillaRepository; | ||||
|             _precioRepository = precioRepository; | ||||
|             _recargoZonaRepository = recargoZonaRepository; | ||||
|             _porcMonCanillaRepository = porcMonCanillaRepository; | ||||
|             _usuarioRepository = usuarioRepository; | ||||
|             _connectionFactory = connectionFactory; | ||||
|             _logger = logger; | ||||
|         } | ||||
|  | ||||
|         private async Task<EntradaSalidaCanillaDto?> MapToDto(EntradaSalidaCanilla? es) | ||||
|         { | ||||
|             if (es == null) return null; | ||||
|  | ||||
|             var publicacionDataResult = await _publicacionRepository.GetByIdAsync(es.IdPublicacion); | ||||
|             var canillaDataResult = await _canillaRepository.GetByIdAsync(es.IdCanilla); // Devuelve tupla | ||||
|  | ||||
|             Publicacion? publicacionEntity = publicacionDataResult.Publicacion; // Componente nullable de la tupla | ||||
|             Canilla? canillaEntity = canillaDataResult.Canilla; // Componente nullable de la tupla | ||||
|  | ||||
|             var usuarioLiq = es.UserLiq.HasValue ? await _usuarioRepository.GetByIdAsync(es.UserLiq.Value) : null; | ||||
|  | ||||
|             decimal montoARendir = await CalcularMontoARendir(es, canillaEntity, publicacionEntity); | ||||
|  | ||||
|             return new EntradaSalidaCanillaDto | ||||
|             { | ||||
|                 IdParte = es.IdParte, | ||||
|                 IdPublicacion = es.IdPublicacion, | ||||
|                 NombrePublicacion = publicacionEntity?.Nombre ?? "Pub. Desconocida", | ||||
|                 IdCanilla = es.IdCanilla, | ||||
|                 NomApeCanilla = canillaEntity?.NomApe ?? "Can. Desconocido", | ||||
|                 CanillaEsAccionista = canillaEntity?.Accionista ?? false, | ||||
|                 Fecha = es.Fecha.ToString("yyyy-MM-dd"), | ||||
|                 CantSalida = es.CantSalida, | ||||
|                 CantEntrada = es.CantEntrada, | ||||
|                 Vendidos = es.CantSalida - es.CantEntrada, | ||||
|                 Observacion = es.Observacion, | ||||
|                 Liquidado = es.Liquidado, | ||||
|                 FechaLiquidado = es.FechaLiquidado?.ToString("yyyy-MM-dd"), | ||||
|                 UserLiq = es.UserLiq, | ||||
|                 NombreUserLiq = usuarioLiq != null ? $"{usuarioLiq.Nombre} {usuarioLiq.Apellido}" : null, | ||||
|                 MontoARendir = montoARendir, | ||||
|                 PrecioUnitarioAplicado = (await _precioRepository.GetByIdAsync(es.IdPrecio))?.Lunes ?? 0, | ||||
|                 RecargoAplicado = es.IdRecargo > 0 ? (await _recargoZonaRepository.GetByIdAsync(es.IdRecargo))?.Valor ?? 0 : 0, | ||||
|                 PorcentajeOMontoCanillaAplicado = es.IdPorcMon > 0 ? (await _porcMonCanillaRepository.GetByIdAsync(es.IdPorcMon))?.PorcMon ?? 0 : 0, | ||||
|                 EsPorcentajeCanilla = es.IdPorcMon > 0 ? (await _porcMonCanillaRepository.GetByIdAsync(es.IdPorcMon))?.EsPorcentaje ?? false : false | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         private async Task<decimal> CalcularMontoARendir(EntradaSalidaCanilla es, Canilla? canilla, Publicacion? publicacion) // Acepta nullable Canilla y Publicacion | ||||
|         { | ||||
|             if (es.CantSalida - es.CantEntrada <= 0) return 0; | ||||
|  | ||||
|             var precioConfig = await _precioRepository.GetByIdAsync(es.IdPrecio); | ||||
|             if (precioConfig == null) | ||||
|             { | ||||
|                 _logger.LogError("Configuración de precio ID {IdPrecio} no encontrada para movimiento ID {IdParte}.", es.IdPrecio, es.IdParte); | ||||
|                 throw new InvalidOperationException($"Configuración de precio ID {es.IdPrecio} no encontrada para el movimiento."); | ||||
|             } | ||||
|  | ||||
|             decimal precioDia = 0; | ||||
|             DayOfWeek diaSemana = es.Fecha.DayOfWeek; | ||||
|             switch (diaSemana) | ||||
|             { | ||||
|                 case DayOfWeek.Monday: precioDia = precioConfig.Lunes ?? 0; break; | ||||
|                 case DayOfWeek.Tuesday: precioDia = precioConfig.Martes ?? 0; break; | ||||
|                 case DayOfWeek.Wednesday: precioDia = precioConfig.Miercoles ?? 0; break; | ||||
|                 case DayOfWeek.Thursday: precioDia = precioConfig.Jueves ?? 0; break; | ||||
|                 case DayOfWeek.Friday: precioDia = precioConfig.Viernes ?? 0; break; | ||||
|                 case DayOfWeek.Saturday: precioDia = precioConfig.Sabado ?? 0; break; | ||||
|                 case DayOfWeek.Sunday: precioDia = precioConfig.Domingo ?? 0; break; | ||||
|             } | ||||
|  | ||||
|             decimal valorRecargo = 0; | ||||
|             if (es.IdRecargo > 0) | ||||
|             { | ||||
|                 var recargoConfig = await _recargoZonaRepository.GetByIdAsync(es.IdRecargo); | ||||
|                 if (recargoConfig != null) valorRecargo = recargoConfig.Valor; | ||||
|             } | ||||
|  | ||||
|             decimal precioFinalUnitario = precioDia + valorRecargo; | ||||
|             int cantidadVendida = es.CantSalida - es.CantEntrada; | ||||
|  | ||||
|             if (canilla != null && canilla.Accionista && es.IdPorcMon > 0) // Check null para canilla | ||||
|             { | ||||
|                 var porcMonConfig = await _porcMonCanillaRepository.GetByIdAsync(es.IdPorcMon); | ||||
|                 if (porcMonConfig != null) | ||||
|                 { | ||||
|                     if (porcMonConfig.EsPorcentaje) | ||||
|                     { | ||||
|                         return Math.Round((precioFinalUnitario * cantidadVendida * porcMonConfig.PorcMon) / 100, 2); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         return Math.Round(cantidadVendida * porcMonConfig.PorcMon, 2); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             return Math.Round(precioFinalUnitario * cantidadVendida, 2); | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<EntradaSalidaCanillaDto>> ObtenerTodosAsync( | ||||
|             DateTime? fechaDesde, DateTime? fechaHasta, | ||||
|             int? idPublicacion, int? idCanilla, bool? liquidados, bool? incluirNoLiquidados) | ||||
|         { | ||||
|             bool? filtroLiquidadoFinal = null; | ||||
|             if (liquidados.HasValue) | ||||
|             { | ||||
|                 filtroLiquidadoFinal = liquidados.Value; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 if (incluirNoLiquidados.HasValue && !incluirNoLiquidados.Value) | ||||
|                 { | ||||
|                     filtroLiquidadoFinal = true; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             var movimientos = await _esCanillaRepository.GetAllAsync(fechaDesde, fechaHasta, idPublicacion, idCanilla, filtroLiquidadoFinal); | ||||
|             var dtos = new List<EntradaSalidaCanillaDto>(); | ||||
|             foreach (var mov in movimientos) | ||||
|             { | ||||
|                 var dto = await MapToDto(mov); | ||||
|                 if (dto != null) dtos.Add(dto); | ||||
|             } | ||||
|             return dtos; | ||||
|         } | ||||
|  | ||||
|         public async Task<EntradaSalidaCanillaDto?> ObtenerPorIdAsync(int idParte) | ||||
|         { | ||||
|             var movimiento = await _esCanillaRepository.GetByIdAsync(idParte); | ||||
|             return await MapToDto(movimiento); | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> ActualizarMovimientoAsync(int idParte, UpdateEntradaSalidaCanillaDto updateDto, int idUsuario) | ||||
|         { | ||||
|             using var connection = _connectionFactory.CreateConnection(); | ||||
|             if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync(); else if (connection.State == ConnectionState.Closed) connection.Open(); | ||||
|             using var transaction = connection.BeginTransaction(); | ||||
|             try | ||||
|             { | ||||
|                 var esExistente = await _esCanillaRepository.GetByIdAsync(idParte); | ||||
|                 if (esExistente == null) return (false, "Movimiento no encontrado."); | ||||
|                 if (esExistente.Liquidado) return (false, "No se puede modificar un movimiento ya liquidado."); | ||||
|  | ||||
|                 esExistente.CantSalida = updateDto.CantSalida; | ||||
|                 esExistente.CantEntrada = updateDto.CantEntrada; | ||||
|                 esExistente.Observacion = updateDto.Observacion; | ||||
|  | ||||
|                 var actualizado = await _esCanillaRepository.UpdateAsync(esExistente, idUsuario, transaction); | ||||
|                 if (!actualizado) throw new DataException("Error al actualizar el movimiento."); | ||||
|  | ||||
|                 transaction.Commit(); | ||||
|                 _logger.LogInformation("Movimiento Canillita ID {Id} actualizado por Usuario ID {UserId}.", idParte, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (KeyNotFoundException) { if (transaction.Connection != null) try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 if (transaction.Connection != null) try { transaction.Rollback(); } catch { } | ||||
|                 _logger.LogError(ex, "Error ActualizarMovimientoAsync Canillita ID: {Id}", idParte); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 if (connection?.State == ConnectionState.Open) | ||||
|                 { | ||||
|                     if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> EliminarMovimientoAsync(int idParte, int idUsuario, ClaimsPrincipal userPrincipal) | ||||
|         { | ||||
|             // Helper interno para verificar permisos desde el ClaimsPrincipal proporcionado | ||||
|             bool TienePermisoEspecifico(string codAcc) => userPrincipal.IsInRole("SuperAdmin") || userPrincipal.HasClaim(c => c.Type == "permission" && c.Value == codAcc); | ||||
|  | ||||
|             using var connection = _connectionFactory.CreateConnection(); | ||||
|             IDbTransaction? transaction = null; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync(); | ||||
|                 else if (connection.State == ConnectionState.Closed) connection.Open(); | ||||
|                  | ||||
|                 transaction = connection.BeginTransaction(); | ||||
|  | ||||
|                 var esExistente = await _esCanillaRepository.GetByIdAsync(idParte); | ||||
|                 if (esExistente == null) | ||||
|                 { | ||||
|                     if (transaction?.Connection != null) transaction.Rollback(); | ||||
|                     return (false, "Movimiento no encontrado."); | ||||
|                 } | ||||
|  | ||||
|                 if (esExistente.Liquidado) | ||||
|                 { | ||||
|                     // Permiso MC006 es para "Eliminar Movimientos de Canillita Liquidados" | ||||
|                     if (!TienePermisoEspecifico("MC006")) | ||||
|                     { | ||||
|                         if (transaction?.Connection != null) transaction.Rollback(); | ||||
|                         return (false, "No tiene permiso para eliminar movimientos ya liquidados. Se requiere permiso especial (MC006) o ser SuperAdmin."); | ||||
|                     } | ||||
|                     _logger.LogWarning("Usuario ID {IdUsuario} está eliminando un movimiento LIQUIDADO (IDParte: {IdParte}). Permiso MC006 verificado.", idUsuario, idParte); | ||||
|                 } | ||||
|                 // Si no está liquidado, el permiso MC004 ya fue verificado en el controlador. | ||||
|  | ||||
|                 var eliminado = await _esCanillaRepository.DeleteAsync(idParte, idUsuario, transaction); | ||||
|                 if (!eliminado) | ||||
|                 { | ||||
|                     // No es necesario hacer rollback aquí si DeleteAsync lanza una excepción, | ||||
|                     // ya que el bloque catch lo manejará. Si DeleteAsync devuelve false sin lanzar, | ||||
|                     // entonces sí sería necesario un rollback. | ||||
|                     if (transaction?.Connection != null) transaction.Rollback(); | ||||
|                     throw new DataException("Error al eliminar el movimiento desde el repositorio."); | ||||
|                 } | ||||
|  | ||||
|                 if (transaction?.Connection != null) transaction.Commit(); | ||||
|                 _logger.LogInformation("Movimiento Canillita ID {IdParte} eliminado por Usuario ID {IdUsuario}.", idParte, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (KeyNotFoundException)  | ||||
|             {  | ||||
|                 if (transaction?.Connection != null) try { transaction.Rollback(); } catch (Exception exR) { _logger.LogError(exR, "Rollback fallido KeyNotFoundException."); } | ||||
|                 return (false, "Movimiento no encontrado.");  | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 if (transaction?.Connection != null) { try { transaction.Rollback(); } catch (Exception exRollback) { _logger.LogError(exRollback, "Error durante rollback de transacción."); } } | ||||
|                 _logger.LogError(ex, "Error EliminarMovimientoAsync Canillita ID: {IdParte}", idParte); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 if (transaction != null) transaction.Dispose(); | ||||
|                 if (connection?.State == ConnectionState.Open) | ||||
|                 { | ||||
|                     if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> LiquidarMovimientosAsync(LiquidarMovimientosCanillaRequestDto liquidarDto, int idUsuarioLiquidador) | ||||
|         { | ||||
|             if (liquidarDto.IdsPartesALiquidar == null || !liquidarDto.IdsPartesALiquidar.Any()) | ||||
|                 return (false, "No se especificaron movimientos para liquidar."); | ||||
|  | ||||
|             using var connection = _connectionFactory.CreateConnection(); | ||||
|             if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync(); else if (connection.State == ConnectionState.Closed) connection.Open(); | ||||
|             using var transaction = connection.BeginTransaction(); | ||||
|             try | ||||
|             { | ||||
|                 bool liquidacionExitosa = await _esCanillaRepository.LiquidarAsync(liquidarDto.IdsPartesALiquidar, liquidarDto.FechaLiquidacion.Date, idUsuarioLiquidador, transaction); | ||||
|  | ||||
|                 if (!liquidacionExitosa) | ||||
|                 { | ||||
|                     _logger.LogWarning("Liquidación de movimientos de canillita pudo no haber afectado a todos los IDs solicitados. IDs: {Ids}", string.Join(",", liquidarDto.IdsPartesALiquidar)); | ||||
|                 } | ||||
|  | ||||
|                 if (transaction.Connection != null) transaction.Commit(); | ||||
|                 _logger.LogInformation("Movimientos de Canillita liquidados. Ids: {Ids} por Usuario ID {UserId}.", string.Join(",", liquidarDto.IdsPartesALiquidar), idUsuarioLiquidador); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 if (transaction.Connection != null) { try { transaction.Rollback(); } catch (Exception exRollback) { _logger.LogError(exRollback, "Error durante rollback de transacción."); } } | ||||
|                 _logger.LogError(ex, "Error al liquidar movimientos de canillita."); | ||||
|                 return (false, $"Error interno al liquidar movimientos: {ex.Message}"); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 if (connection?.State == ConnectionState.Open) | ||||
|                 { | ||||
|                     if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(IEnumerable<EntradaSalidaCanillaDto>? MovimientosCreados, string? Error)> CrearMovimientosEnLoteAsync(CreateBulkEntradaSalidaCanillaDto createBulkDto, int idUsuario) | ||||
|         { | ||||
|             var canillaDataResult = await _canillaRepository.GetByIdAsync(createBulkDto.IdCanilla); | ||||
|             Canilla? canillaActual = canillaDataResult.Canilla; | ||||
|             if (canillaActual == null) return (null, "Canillita no válido."); | ||||
|             if (canillaActual.Baja) return (null, "El canillita está dado de baja."); | ||||
|  | ||||
|             List<EntradaSalidaCanilla> movimientosCreadosEntidades = new List<EntradaSalidaCanilla>(); | ||||
|             List<string> erroresItems = new List<string>(); | ||||
|  | ||||
|             using var connection = _connectionFactory.CreateConnection(); | ||||
|             if (connection is System.Data.Common.DbConnection dbConnOpen && connection.State == ConnectionState.Closed) await dbConnOpen.OpenAsync(); else if (connection.State == ConnectionState.Closed) connection.Open(); | ||||
|             using var transaction = connection.BeginTransaction(); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 foreach (var itemDto in createBulkDto.Items.Where(i => i.IdPublicacion > 0)) | ||||
|                 { | ||||
|                     if (await _esCanillaRepository.ExistsByPublicacionCanillaFechaAsync(itemDto.IdPublicacion, createBulkDto.IdCanilla, createBulkDto.Fecha.Date, transaction: transaction)) | ||||
|                     { | ||||
|                         var pubInfo = await _publicacionRepository.GetByIdSimpleAsync(itemDto.IdPublicacion); | ||||
|                         erroresItems.Add($"Ya existe un registro para la publicación '{pubInfo?.Nombre ?? itemDto.IdPublicacion.ToString()}' para {canillaActual.NomApe} en la fecha {createBulkDto.Fecha:dd/MM/yyyy}."); | ||||
|                     } | ||||
|                 } | ||||
|                 if (erroresItems.Any()) | ||||
|                 { | ||||
|                     if (transaction.Connection != null) transaction.Rollback(); | ||||
|                     return (null, string.Join(" ", erroresItems)); | ||||
|                 } | ||||
|  | ||||
|                 foreach (var itemDto in createBulkDto.Items) | ||||
|                 { | ||||
|                     if (itemDto.IdPublicacion == 0 && itemDto.CantSalida == 0 && itemDto.CantEntrada == 0 && string.IsNullOrWhiteSpace(itemDto.Observacion)) continue; | ||||
|                     if (itemDto.IdPublicacion == 0) | ||||
|                     { | ||||
|                         if (itemDto.CantSalida > 0 || itemDto.CantEntrada > 0 || !string.IsNullOrWhiteSpace(itemDto.Observacion)) | ||||
|                         { | ||||
|                             erroresItems.Add($"Falta seleccionar la publicación para una de las líneas con cantidades/observación."); | ||||
|                         } | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     var publicacionItem = await _publicacionRepository.GetByIdSimpleAsync(itemDto.IdPublicacion); | ||||
|                     bool noEsValidaONoHabilitada = false; | ||||
|                     if (publicacionItem == null) | ||||
|                     { | ||||
|                         noEsValidaONoHabilitada = true; | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         // Si Habilitada es bool? y NULL significa que toma el DEFAULT 1 (true) de la BD | ||||
|                         // entonces solo consideramos error si es explícitamente false. | ||||
|                         if (publicacionItem.Habilitada.HasValue && publicacionItem.Habilitada.Value == false) | ||||
|                         { | ||||
|                             noEsValidaONoHabilitada = true; | ||||
|                         } | ||||
|                         // Si publicacionItem.Habilitada es null o true, noEsValidaONoHabilitada permanece false. | ||||
|                     } | ||||
|  | ||||
|                     if (noEsValidaONoHabilitada) | ||||
|                     { | ||||
|                         erroresItems.Add($"Publicación ID {itemDto.IdPublicacion} no es válida o no está habilitada."); | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(itemDto.IdPublicacion, createBulkDto.Fecha.Date, transaction); | ||||
|                     if (precioActivo == null) | ||||
|                     { | ||||
|                         string nombrePubParaError = publicacionItem?.Nombre ?? $"ID {itemDto.IdPublicacion}"; | ||||
|                         erroresItems.Add($"No hay precio definido para '{nombrePubParaError}' en {createBulkDto.Fecha:dd/MM/yyyy}."); | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     RecargoZona? recargoActivo = null; | ||||
|                     // Aquí usamos canillaActual! porque ya verificamos que no es null al inicio del método. | ||||
|                     if (canillaActual!.IdZona > 0) | ||||
|                     { | ||||
|                         recargoActivo = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(itemDto.IdPublicacion, canillaActual.IdZona, createBulkDto.Fecha.Date, transaction); | ||||
|                     } | ||||
|  | ||||
|                     PorcMonCanilla? porcMonActivo = null; | ||||
|                     // Aquí usamos canillaActual! porque ya verificamos que no es null al inicio del método. | ||||
|                     if (canillaActual!.Accionista) | ||||
|                     { | ||||
|                         porcMonActivo = await _porcMonCanillaRepository.GetActiveByPublicacionCanillaAndDateAsync(itemDto.IdPublicacion, createBulkDto.IdCanilla, createBulkDto.Fecha.Date, transaction); | ||||
|                         if (porcMonActivo == null) | ||||
|                         { | ||||
|                             // Dentro de este bloque, canillaActual.NomApe es seguro porque Accionista era true. | ||||
|                             string nombreCanParaError = canillaActual.NomApe; | ||||
|                             string nombrePubParaError = publicacionItem?.Nombre ?? $"Publicación ID {itemDto.IdPublicacion}"; | ||||
|                             erroresItems.Add($"'{nombreCanParaError}' es accionista pero no tiene %/monto para '{nombrePubParaError}' en {createBulkDto.Fecha:dd/MM/yyyy}."); | ||||
|                             continue; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     var nuevoES = new EntradaSalidaCanilla | ||||
|                     { | ||||
|                         IdPublicacion = itemDto.IdPublicacion, | ||||
|                         IdCanilla = createBulkDto.IdCanilla, | ||||
|                         Fecha = createBulkDto.Fecha.Date, | ||||
|                         CantSalida = itemDto.CantSalida, | ||||
|                         CantEntrada = itemDto.CantEntrada, | ||||
|                         Observacion = itemDto.Observacion, | ||||
|                         IdPrecio = precioActivo.IdPrecio, | ||||
|                         IdRecargo = recargoActivo?.IdRecargo ?? 0, | ||||
|                         IdPorcMon = porcMonActivo?.IdPorcMon ?? 0, | ||||
|                         Liquidado = false, | ||||
|                         FechaLiquidado = null, | ||||
|                         UserLiq = null | ||||
|                     }; | ||||
|  | ||||
|                     var esCreada = await _esCanillaRepository.CreateAsync(nuevoES, idUsuario, transaction); | ||||
|                     if (esCreada == null) throw new DataException($"Error al registrar movimiento para Publicación ID {itemDto.IdPublicacion}."); | ||||
|                     movimientosCreadosEntidades.Add(esCreada); | ||||
|                 } | ||||
|  | ||||
|                 if (erroresItems.Any()) | ||||
|                 { | ||||
|                     if (transaction.Connection != null) transaction.Rollback(); | ||||
|                     return (null, string.Join(" ", erroresItems)); | ||||
|                 } | ||||
|  | ||||
|                 // CORRECCIÓN PARA CS0019 (línea 394 original): | ||||
|                 bool tieneItemsSignificativos = false; | ||||
|                 if (createBulkDto.Items != null) // Checkear si Items es null antes de llamar a Any() | ||||
|                 { | ||||
|                     tieneItemsSignificativos = createBulkDto.Items.Any(i => i.IdPublicacion > 0 && | ||||
|                                                                     (i.CantSalida > 0 || i.CantEntrada > 0 || !string.IsNullOrWhiteSpace(i.Observacion))); | ||||
|                 } | ||||
|  | ||||
|                 if (!movimientosCreadosEntidades.Any() && tieneItemsSignificativos) | ||||
|                 { | ||||
|                     if (transaction.Connection != null) transaction.Rollback(); | ||||
|                     return (null, "No se pudo procesar ningún ítem válido con datos significativos."); | ||||
|                 } | ||||
|  | ||||
|                 if (transaction.Connection != null) transaction.Commit(); | ||||
|                 _logger.LogInformation("Lote de {Count} movimientos Canillita para Canilla ID {IdCanilla} en Fecha {Fecha} creados por Usuario ID {UserId}.", | ||||
|                     movimientosCreadosEntidades.Count, createBulkDto.IdCanilla, createBulkDto.Fecha.Date, idUsuario); | ||||
|  | ||||
|                 var dtosCreados = new List<EntradaSalidaCanillaDto>(); | ||||
|                 foreach (var entidad in movimientosCreadosEntidades) | ||||
|                 { | ||||
|                     var dto = await MapToDto(entidad); | ||||
|                     if (dto != null) dtosCreados.Add(dto); | ||||
|                 } | ||||
|                 return (dtosCreados, null); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 if (transaction.Connection != null) { try { transaction.Rollback(); } catch (Exception exRollback) { _logger.LogError(exRollback, "Error durante rollback de transacción."); } } | ||||
|                 _logger.LogError(ex, "Error CrearMovimientosEnLoteAsync para Canilla ID {IdCanilla}, Fecha {Fecha}", createBulkDto.IdCanilla, createBulkDto.Fecha); | ||||
|                 return (null, $"Error interno al procesar el lote: {ex.Message}"); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 if (connection?.State == ConnectionState.Open) | ||||
|                 { | ||||
|                     if (connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); else connection.Close(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -57,9 +57,29 @@ namespace GestionIntegral.Api.Services.Distribucion | ||||
|             var publicacionData = await _publicacionRepository.GetByIdAsync(es.IdPublicacion); | ||||
|             var distribuidorData = await _distribuidorRepository.GetByIdAsync(es.IdDistribuidor); | ||||
|              | ||||
|             decimal montoCalculado = await CalcularMontoMovimiento(es.IdPublicacion, es.IdDistribuidor, es.Fecha, es.Cantidad, es.TipoMovimiento, | ||||
|                                                                  es.IdPrecio, es.IdRecargo, es.IdPorcentaje, distribuidorData.Distribuidor?.IdZona); | ||||
|             // Obtener el valor bruto del movimiento | ||||
|             decimal valorBrutoMovimiento = await CalcularMontoMovimiento( | ||||
|                 es.IdPublicacion, es.IdDistribuidor, es.Fecha, es.Cantidad,  | ||||
|                 es.TipoMovimiento, // Pasamos el tipo de movimiento original aquí | ||||
|                 es.IdPrecio, es.IdRecargo, es.IdPorcentaje, distribuidorData.Distribuidor?.IdZona | ||||
|             ); | ||||
|  | ||||
|             // Ajustar para el DTO: si es "Entrada", el monto calculado es un crédito (negativo o positivo según convención) | ||||
|             // Para consistencia con el ajuste de saldo, si es Entrada, el MontoCalculado para el DTO puede ser el valor | ||||
|             // que se le "acredita" al distribuidor (o sea, el valor de la mercadería devuelta). | ||||
|             // La lógica de +/- para el saldo ya está en Crear/Actualizar/Eliminar. | ||||
|             // Aquí solo mostramos el valor del movimiento. Si es entrada, es el valor de lo devuelto. | ||||
|             // Si es salida, es el valor de lo que se le factura. | ||||
|             // El método CalcularMonto ya devuelve el monto que el distribuidor DEBE pagar por una SALIDA. | ||||
|             // Para una ENTRADA (devolución), el valor de esa mercadería es el mismo, pero opera en sentido contrario al saldo. | ||||
|  | ||||
|             decimal montoCalculadoParaDto = valorBrutoMovimiento; | ||||
|             // Si queremos que el DTO muestre las entradas como un valor que "reduce la deuda", | ||||
|             // podría ser positivo. Si queremos que refleje el impacto directo en la factura (salidas suman, entradas restan), | ||||
|             // podríamos hacerlo negativo. | ||||
|             // Por ahora, dejaremos que CalcularMontoMovimiento devuelva el valor de una "Salida", | ||||
|             // y si es "Entrada", este mismo valor es el que se acredita. | ||||
|             // La columna `MontoCalculado` en el DTO representará el valor de la transacción. | ||||
|  | ||||
|             return new EntradaSalidaDistDto | ||||
|             { | ||||
| @@ -75,17 +95,29 @@ namespace GestionIntegral.Api.Services.Distribucion | ||||
|                 Cantidad = es.Cantidad, | ||||
|                 Remito = es.Remito, | ||||
|                 Observacion = es.Observacion, | ||||
|                 MontoCalculado = montoCalculado | ||||
|                 MontoCalculado = montoCalculadoParaDto // Representa el valor de los N ejemplares | ||||
|             }; | ||||
|         } | ||||
|          | ||||
|         private async Task<decimal> CalcularMontoMovimiento(int idPublicacion, int idDistribuidor, DateTime fecha, int cantidad, string tipoMovimiento, | ||||
|                                                             int idPrecio, int idRecargo, int idPorcentaje, int? idZonaDistribuidor) | ||||
|         { | ||||
|             if (tipoMovimiento == "Entrada") return 0; // Las entradas (devoluciones) no generan "debe" inmediato, ajustan el saldo. | ||||
|             // YA NO SE DEVUELVE 0 PARA ENTRADA AQUÍ | ||||
|             // if (tipoMovimiento == "Entrada") return 0;  | ||||
|  | ||||
|             var precioConfig = await _precioRepository.GetByIdAsync(idPrecio); | ||||
|             if (precioConfig == null) throw new InvalidOperationException("Configuración de precio no encontrada para el movimiento."); | ||||
|             // Es crucial que idPrecio sea válido y se haya determinado correctamente antes de llamar aquí. | ||||
|             // Si precioConfig es null, se lanzará una excepción abajo, lo cual está bien si es un estado inesperado. | ||||
|             if (precioConfig == null) | ||||
|             { | ||||
|                 _logger.LogError("Configuración de precio ID {IdPrecio} no encontrada al calcular monto para Pub {IdPublicacion}, Dist {IdDistribuidor}, Fecha {Fecha}", idPrecio, idPublicacion, idDistribuidor, fecha); | ||||
|                 // Dependiendo de la regla de negocio, podrías devolver 0 o lanzar una excepción. | ||||
|                 // Si un precio es OBLIGATORIO para cualquier movimiento, lanzar excepción es más apropiado. | ||||
|                 // Si puede haber movimientos sin precio (ej. cortesía que no se factura), entonces 0. | ||||
|                 // En este contexto, un precio es fundamental para el cálculo. | ||||
|                 throw new InvalidOperationException($"Configuración de precio ID {idPrecio} no encontrada. No se puede calcular el monto."); | ||||
|             } | ||||
|  | ||||
|  | ||||
|             decimal precioDia = 0; | ||||
|             DayOfWeek diaSemana = fecha.DayOfWeek; | ||||
| @@ -101,9 +133,8 @@ namespace GestionIntegral.Api.Services.Distribucion | ||||
|             } | ||||
|  | ||||
|             decimal valorRecargo = 0; | ||||
|             if (idRecargo > 0 && idZonaDistribuidor.HasValue) // El recargo se aplica por la zona del distribuidor | ||||
|             if (idRecargo > 0 && idZonaDistribuidor.HasValue) | ||||
|             { | ||||
|                 // Necesitamos encontrar el recargo activo para la publicación, la zona del distribuidor y la fecha | ||||
|                  var recargoConfig = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(idPublicacion, idZonaDistribuidor.Value, fecha); | ||||
|                 if (recargoConfig != null) | ||||
|                 { | ||||
| @@ -119,10 +150,13 @@ namespace GestionIntegral.Api.Services.Distribucion | ||||
|                 var porcConfig = await _porcPagoRepository.GetByIdAsync(idPorcentaje); | ||||
|                 if (porcConfig != null) | ||||
|                 { | ||||
|                     return (montoBase / 100) * porcConfig.Porcentaje; | ||||
|                     // El porcentaje de pago del distribuidor es lo que ÉL PAGA a la editorial. | ||||
|                     // Entonces, el monto es (precio_tapa_con_recargo * cantidad) * (porcentaje_pago_dist / 100) | ||||
|                     return (montoBase * porcConfig.Porcentaje) / 100; | ||||
|                 } | ||||
|             } | ||||
|             return montoBase; // Si no hay porcentaje, se factura el 100% del precio con recargo | ||||
|             // Si no hay porcentaje de pago específico, se asume que el distribuidor paga el 100% del monto base. | ||||
|             return montoBase; | ||||
|         } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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 IControlDevolucionesService | ||||
|     { | ||||
|         Task<IEnumerable<ControlDevolucionesDto>> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idEmpresa); | ||||
|         Task<ControlDevolucionesDto?> ObtenerPorIdAsync(int idControl); | ||||
|         Task<(ControlDevolucionesDto? Control, string? Error)> CrearAsync(CreateControlDevolucionesDto createDto, int idUsuario); | ||||
|         Task<(bool Exito, string? Error)> ActualizarAsync(int idControl, UpdateControlDevolucionesDto updateDto, int idUsuario); | ||||
|         Task<(bool Exito, string? Error)> EliminarAsync(int idControl, int idUsuario); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| using GestionIntegral.Api.Dtos.Distribucion; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Security.Claims; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Distribucion | ||||
| { | ||||
|     public interface IEntradaSalidaCanillaService | ||||
|     { | ||||
|         Task<IEnumerable<EntradaSalidaCanillaDto>> ObtenerTodosAsync( | ||||
|             DateTime? fechaDesde, DateTime? fechaHasta, | ||||
|             int? idPublicacion, int? idCanilla, bool? liquidados, bool? incluirNoLiquidados); | ||||
|  | ||||
|         Task<EntradaSalidaCanillaDto?> ObtenerPorIdAsync(int idParte); | ||||
|         Task<(IEnumerable<EntradaSalidaCanillaDto>? MovimientosCreados, string? Error)> CrearMovimientosEnLoteAsync(CreateBulkEntradaSalidaCanillaDto createBulkDto, int idUsuario); | ||||
|         Task<(bool Exito, string? Error)> ActualizarMovimientoAsync(int idParte, UpdateEntradaSalidaCanillaDto updateDto, int idUsuario); | ||||
|  | ||||
|         Task<(bool Exito, string? Error)> EliminarMovimientoAsync(int idParte, int idUsuario, ClaimsPrincipal userPrincipal); | ||||
|  | ||||
|         Task<(bool Exito, string? Error)> LiquidarMovimientosAsync(LiquidarMovimientosCanillaRequestDto liquidarDto, int idUsuarioLiquidador); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										173
									
								
								Backend/GestionIntegral.Api/Services/Radios/CancionService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								Backend/GestionIntegral.Api/Services/Radios/CancionService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| using GestionIntegral.Api.Data.Repositories.Radios; | ||||
| using GestionIntegral.Api.Dtos.Radios; | ||||
| using GestionIntegral.Api.Models.Radios; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| // using GestionIntegral.Api.Data; // Para DbConnectionFactory si se usa transacción | ||||
| // using System.Data; // Para IsolationLevel si se usa transacción | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Radios | ||||
| { | ||||
|     public class CancionService : ICancionService | ||||
|     { | ||||
|         private readonly ICancionRepository _cancionRepository; | ||||
|         private readonly IRitmoRepository _ritmoRepository; | ||||
|         private readonly ILogger<CancionService> _logger; | ||||
|  | ||||
|         public CancionService( | ||||
|             ICancionRepository cancionRepository, | ||||
|             IRitmoRepository ritmoRepository, | ||||
|             ILogger<CancionService> logger) | ||||
|         { | ||||
|             _cancionRepository = cancionRepository; | ||||
|             _ritmoRepository = ritmoRepository; | ||||
|             _logger = logger; | ||||
|         } | ||||
|  | ||||
|         private async Task<CancionDto> MapToDto(Cancion cancion) | ||||
|         { | ||||
|             if (cancion == null) return null!; | ||||
|  | ||||
|             string? nombreRitmo = null; | ||||
|             if (cancion.Ritmo.HasValue && cancion.Ritmo.Value > 0) | ||||
|             { | ||||
|                 var ritmoDb = await _ritmoRepository.GetByIdAsync(cancion.Ritmo.Value); | ||||
|                 nombreRitmo = ritmoDb?.NombreRitmo; | ||||
|             } | ||||
|  | ||||
|             return new CancionDto | ||||
|             { | ||||
|                 Id = cancion.Id, | ||||
|                 Tema = cancion.Tema, | ||||
|                 CompositorAutor = cancion.CompositorAutor, | ||||
|                 Interprete = cancion.Interprete, | ||||
|                 Sello = cancion.Sello, | ||||
|                 Placa = cancion.Placa, | ||||
|                 Pista = cancion.Pista, | ||||
|                 Introduccion = cancion.Introduccion, | ||||
|                 IdRitmo = cancion.Ritmo, // Pasar el valor de cancion.Ritmo | ||||
|                 NombreRitmo = nombreRitmo, | ||||
|                 Formato = cancion.Formato, | ||||
|                 Album = cancion.Album | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<CancionDto>> ObtenerTodasAsync(string? temaFilter, string? interpreteFilter, int? idRitmoFilter) | ||||
|         { | ||||
|             var canciones = await _cancionRepository.GetAllAsync(temaFilter, interpreteFilter, idRitmoFilter); | ||||
|             var dtos = new List<CancionDto>(); | ||||
|             foreach (var cancion in canciones) | ||||
|             { | ||||
|                 dtos.Add(await MapToDto(cancion)); | ||||
|             } | ||||
|             return dtos; | ||||
|         } | ||||
|  | ||||
|         public async Task<CancionDto?> ObtenerPorIdAsync(int id) | ||||
|         { | ||||
|             var cancion = await _cancionRepository.GetByIdAsync(id); | ||||
|             return cancion == null ? null : await MapToDto(cancion); | ||||
|         } | ||||
|  | ||||
|         public async Task<(CancionDto? Cancion, string? Error)> CrearAsync(CreateCancionDto createDto, int idUsuario) | ||||
|         { | ||||
|             if (createDto.IdRitmo.HasValue && createDto.IdRitmo.Value > 0) // Asegurar que > 0 para evitar buscar ritmo con ID 0 | ||||
|             { | ||||
|                  if(await _ritmoRepository.GetByIdAsync(createDto.IdRitmo.Value) == null) | ||||
|                     return (null, "El ritmo seleccionado no es válido."); | ||||
|             } | ||||
|              | ||||
|             if (!string.IsNullOrWhiteSpace(createDto.Tema) && !string.IsNullOrWhiteSpace(createDto.Interprete) && | ||||
|                 await _cancionRepository.ExistsByTemaAndInterpreteAsync(createDto.Tema, createDto.Interprete)) | ||||
|             { | ||||
|                 return (null, "Ya existe una canción con el mismo tema e intérprete."); | ||||
|             } | ||||
|  | ||||
|             var nuevaCancion = new Cancion | ||||
|             { | ||||
|                 Tema = createDto.Tema, CompositorAutor = createDto.CompositorAutor, Interprete = createDto.Interprete, | ||||
|                 Sello = createDto.Sello, Placa = createDto.Placa, Pista = createDto.Pista, | ||||
|                 Introduccion = createDto.Introduccion, | ||||
|                 Ritmo = createDto.IdRitmo, // Asignar createDto.IdRitmo a la propiedad Ritmo del modelo | ||||
|                 Formato = createDto.Formato, Album = createDto.Album | ||||
|             }; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 var cancionCreada = await _cancionRepository.CreateAsync(nuevaCancion); | ||||
|                 if (cancionCreada == null) return (null, "Error al crear la canción."); | ||||
|  | ||||
|                 _logger.LogInformation("Canción ID {Id} creada por Usuario ID {UserId}.", cancionCreada.Id, idUsuario); | ||||
|                 return (await MapToDto(cancionCreada), null); | ||||
|             } | ||||
|             catch (System.Exception ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error CrearAsync Cancion: {Tema}", createDto.Tema); | ||||
|                 return (null, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCancionDto updateDto, int idUsuario) | ||||
|         { | ||||
|             var cancionExistente = await _cancionRepository.GetByIdAsync(id); | ||||
|             if (cancionExistente == null) return (false, "Canción no encontrada."); | ||||
|  | ||||
|             if (updateDto.IdRitmo.HasValue && updateDto.IdRitmo.Value > 0) // Asegurar que > 0 | ||||
|             { | ||||
|                 if (await _ritmoRepository.GetByIdAsync(updateDto.IdRitmo.Value) == null) | ||||
|                     return (false, "El ritmo seleccionado no es válido."); | ||||
|             } | ||||
|  | ||||
|  | ||||
|             if ((!string.IsNullOrWhiteSpace(updateDto.Tema) && !string.IsNullOrWhiteSpace(updateDto.Interprete)) && | ||||
|                 (cancionExistente.Tema != updateDto.Tema || cancionExistente.Interprete != updateDto.Interprete) && | ||||
|                 await _cancionRepository.ExistsByTemaAndInterpreteAsync(updateDto.Tema!, updateDto.Interprete!, id)) | ||||
|             { | ||||
|                 return (false, "Ya existe otra canción con el mismo tema e intérprete."); // Devolver tupla bool,string | ||||
|             } | ||||
|  | ||||
|             cancionExistente.Tema = updateDto.Tema; cancionExistente.CompositorAutor = updateDto.CompositorAutor; | ||||
|             cancionExistente.Interprete = updateDto.Interprete; cancionExistente.Sello = updateDto.Sello; | ||||
|             cancionExistente.Placa = updateDto.Placa; cancionExistente.Pista = updateDto.Pista; | ||||
|             cancionExistente.Introduccion = updateDto.Introduccion; | ||||
|             cancionExistente.Ritmo = updateDto.IdRitmo; // Asignar updateDto.IdRitmo a la propiedad Ritmo del modelo | ||||
|             cancionExistente.Formato = updateDto.Formato; cancionExistente.Album = updateDto.Album; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 var actualizado = await _cancionRepository.UpdateAsync(cancionExistente); | ||||
|                 if (!actualizado) return (false, "Error al actualizar la canción."); | ||||
|                  | ||||
|                 _logger.LogInformation("Canción ID {Id} actualizada por Usuario ID {UserId}.", id, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (System.Exception ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error ActualizarAsync Canción ID: {Id}", id); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) | ||||
|         { | ||||
|             var cancionExistente = await _cancionRepository.GetByIdAsync(id); | ||||
|             if (cancionExistente == null) return (false, "Canción no encontrada."); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 var eliminado = await _cancionRepository.DeleteAsync(id); | ||||
|                 if (!eliminado) return (false, "Error al eliminar la canción."); | ||||
|  | ||||
|                 _logger.LogInformation("Canción ID {Id} eliminada por Usuario ID {UserId}.", id, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (System.Exception ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error EliminarAsync Canción ID: {Id}", id); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| using GestionIntegral.Api.Dtos.Radios; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Radios | ||||
| { | ||||
|     public interface ICancionService | ||||
|     { | ||||
|         Task<IEnumerable<CancionDto>> ObtenerTodasAsync(string? temaFilter, string? interpreteFilter, int? idRitmoFilter); | ||||
|         Task<CancionDto?> ObtenerPorIdAsync(int id); | ||||
|         Task<(CancionDto? Cancion, string? Error)> CrearAsync(CreateCancionDto createDto, int idUsuario); | ||||
|         Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateCancionDto updateDto, int idUsuario); | ||||
|         Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| using GestionIntegral.Api.Dtos.Radios; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Mvc; // Para IActionResult (o devolver byte[]) | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Radios | ||||
| { | ||||
|     public interface IRadioListaService | ||||
|     { | ||||
|         // Devuelve byte[] para el archivo y el nombre del archivo sugerido | ||||
|         Task<(byte[] FileContents, string ContentType, string FileName, string? Error)> GenerarListaRadioAsync(GenerarListaRadioRequestDto request); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										15
									
								
								Backend/GestionIntegral.Api/Services/Radios/IRitmoService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Backend/GestionIntegral.Api/Services/Radios/IRitmoService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| using GestionIntegral.Api.Dtos.Radios; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Radios | ||||
| { | ||||
|     public interface IRitmoService | ||||
|     { | ||||
|         Task<IEnumerable<RitmoDto>> ObtenerTodosAsync(string? nombreFilter); | ||||
|         Task<RitmoDto?> ObtenerPorIdAsync(int id); | ||||
|         Task<(RitmoDto? Ritmo, string? Error)> CrearAsync(CreateRitmoDto createDto, int idUsuario); | ||||
|         Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateRitmoDto updateDto, int idUsuario); | ||||
|         Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										224
									
								
								Backend/GestionIntegral.Api/Services/Radios/RadioListaService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								Backend/GestionIntegral.Api/Services/Radios/RadioListaService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,224 @@ | ||||
| using GestionIntegral.Api.Data.Repositories.Radios; | ||||
| using GestionIntegral.Api.Dtos.Radios; | ||||
| using GestionIntegral.Api.Models.Radios; // Para Cancion | ||||
| using Microsoft.Extensions.Logging; | ||||
| using NPOI.SS.UserModel; | ||||
| using NPOI.XSSF.UserModel; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.IO.Compression; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Radios | ||||
| { | ||||
|     public class RadioListaService : IRadioListaService | ||||
|     { | ||||
|         private readonly ICancionRepository _cancionRepository; | ||||
|         // private readonly IRitmoRepository _ritmoRepository; // No se usa en la nueva lógica de generación de Excel | ||||
|         private readonly ILogger<RadioListaService> _logger; | ||||
|  | ||||
|         public RadioListaService( | ||||
|             ICancionRepository cancionRepository, | ||||
|             // IRitmoRepository ritmoRepository, // Puede ser removido si no se usa en otro lado | ||||
|             ILogger<RadioListaService> logger) | ||||
|         { | ||||
|             _cancionRepository = cancionRepository; | ||||
|             // _ritmoRepository = ritmoRepository; | ||||
|             _logger = logger; | ||||
|         } | ||||
|  | ||||
|         public async Task<(byte[] FileContents, string ContentType, string FileName, string? Error)> GenerarListaRadioAsync(GenerarListaRadioRequestDto request) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 _logger.LogInformation("Iniciando generación de lista de radio para Mes: {Mes}, Año: {Anio}, Institución: {Institucion}, Radio: {Radio}", | ||||
|                                        request.Mes, request.Anio, request.Institucion, request.Radio); | ||||
|  | ||||
|                 var programacionParaExcel = new List<ProgramacionHorariaExcelDto>(); | ||||
|                 int diasEnMes = DateTime.DaysInMonth(request.Anio, request.Mes); | ||||
|  | ||||
|                 for (int dia = 1; dia <= diasEnMes; dia++) | ||||
|                 { | ||||
|                     int horaInicio = (request.Radio == "FM 99.1") ? 0 : 9; | ||||
|                     int horaFin = 23; | ||||
|  | ||||
|                     for (int hora = horaInicio; hora <= horaFin; hora++) | ||||
|                     { | ||||
|                         int cantidadRegistros; | ||||
|                         if (request.Radio == "FM 99.1") | ||||
|                         { | ||||
|                             cantidadRegistros = (hora == 23 || hora == 7 || hora == 15) ? 1 : 5; | ||||
|                         } | ||||
|                         else // FM 100.3 | ||||
|                         { | ||||
|                             cantidadRegistros = (hora == 23 || hora == 15) ? 1 : 2; | ||||
|                         } | ||||
|  | ||||
|                         if (cantidadRegistros > 0) | ||||
|                         { | ||||
|                             var cancionesSeleccionadas = await _cancionRepository.GetRandomCancionesAsync(cantidadRegistros); | ||||
|                             foreach (var cancion in cancionesSeleccionadas) | ||||
|                             { | ||||
|                                 programacionParaExcel.Add(new ProgramacionHorariaExcelDto | ||||
|                                 { | ||||
|                                     Dia = dia, | ||||
|                                     Hora = hora, | ||||
|                                     TituloObra = cancion.Tema, | ||||
|                                     CompositorAutor = cancion.CompositorAutor, | ||||
|                                     Interprete = cancion.Interprete, | ||||
|                                     Sello = cancion.Sello, | ||||
|                                     Album = cancion.Album | ||||
|                                 }); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (!programacionParaExcel.Any()) | ||||
|                 { | ||||
|                     _logger.LogWarning("No se generaron datos para la lista de radio con los criterios: {@Request}", request); | ||||
|                     return (Array.Empty<byte>(), "", "", "No se generaron datos para la lista con los criterios dados."); | ||||
|                 } | ||||
|  | ||||
|                 string mesText = request.Mes.ToString("00"); | ||||
|                 string anioFullText = request.Anio.ToString(); | ||||
|                 string anioShortText = anioFullText.Length > 2 ? anioFullText.Substring(anioFullText.Length - 2) : anioFullText; | ||||
|  | ||||
|                 byte[] excelBytes = GenerarExcel(programacionParaExcel, request.Institucion, request.Radio, mesText, anioFullText, anioShortText); | ||||
|  | ||||
|                 string baseFileName; | ||||
|                 if (request.Institucion == "AADI") | ||||
|                 { | ||||
|                     baseFileName = request.Radio == "FM 99.1" ? $"AADI-FM99.1-FM-{mesText}{anioShortText}" : $"AADI-FM100.3-FM-{mesText}{anioShortText}"; | ||||
|                 } | ||||
|                 else // SADAIC | ||||
|                 { | ||||
|                     baseFileName = request.Radio == "FM 99.1" ? $"FM99.1-FM-{mesText}{anioShortText}" : $"FM100.3-FM-{mesText}{anioShortText}"; | ||||
|                 } | ||||
|                 string excelFileNameInZip = $"{baseFileName}.xlsx"; | ||||
|                 string zipFileName = $"{baseFileName}.xlsx.zip"; // Para replicar nombre original del zip | ||||
|  | ||||
|                 using (var memoryStream = new MemoryStream()) | ||||
|                 { | ||||
|                     using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) | ||||
|                     { | ||||
|                         var excelEntry = archive.CreateEntry(excelFileNameInZip, CompressionLevel.Optimal); | ||||
|                         using (var entryStream = excelEntry.Open()) | ||||
|                         { | ||||
|                             await entryStream.WriteAsync(excelBytes, 0, excelBytes.Length); | ||||
|                         } | ||||
|                     } | ||||
|                     _logger.LogInformation("Lista de radio generada y empaquetada en ZIP exitosamente: {ZipFileName}", zipFileName); | ||||
|                     return (memoryStream.ToArray(), "application/zip", zipFileName, null); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error al generar la lista de radio para Mes: {Mes}, Año: {Anio}, Institucion: {Institucion}, Radio: {Radio}", request.Mes, request.Anio, request.Institucion, request.Radio); | ||||
|                 return (Array.Empty<byte>(), "", "", $"Error interno al generar la lista: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private byte[] GenerarExcel(List<ProgramacionHorariaExcelDto> programacion, string institucion, string radio, string mesText, string anioFullText, string anioShortText) | ||||
|         { | ||||
|             IWorkbook workbook = new XSSFWorkbook(); | ||||
|  | ||||
|             var coreProps = ((XSSFWorkbook)workbook).GetProperties().CoreProperties; | ||||
|             coreProps.Creator = "GestionIntegral"; // Puedes cambiarlo | ||||
|             coreProps.Title = $"Reporte {institucion} - {radio} - {mesText}/{anioFullText}"; | ||||
|             // coreProps.Description = "Descripción del reporte"; // Opcional | ||||
|             // var extendedProps = ((XSSFWorkbook)workbook).GetProperties().ExtendedProperties.GetUnderlyingProperties(); | ||||
|             // extendedProps.Application = "GestionIntegral System"; // Opcional | ||||
|             // extendedProps.AppVersion = "1.0.0"; // Opcional | ||||
|  | ||||
|             string sheetName; | ||||
|             if (institucion == "AADI") | ||||
|             { | ||||
|                 sheetName = radio == "FM 99.1" ? $"SA99{mesText}{anioShortText}" : $"SARE{mesText}{anioShortText}"; | ||||
|             } | ||||
|             else // SADAIC | ||||
|             { | ||||
|                 sheetName = radio == "FM 99.1" ? $"FM99{mesText}{anioShortText}" : $"FMRE{mesText}{anioShortText}"; | ||||
|             } | ||||
|             ISheet sheet = workbook.CreateSheet(sheetName); | ||||
|  | ||||
|             int currentRowIdx = 0; | ||||
|             if (institucion == "AADI") | ||||
|             { | ||||
|                 sheet.CreateRow(currentRowIdx++).CreateCell(0).SetCellValue(radio == "FM 99.1" ? "Nombre: FM LA 99.1" : "Nombre: FM 100.3 LA REDONDA"); | ||||
|                 sheet.CreateRow(currentRowIdx++).CreateCell(0).SetCellValue("Localidad: La Plata"); | ||||
|                 sheet.CreateRow(currentRowIdx++).CreateCell(0).SetCellValue("FM"); | ||||
|                 sheet.CreateRow(currentRowIdx++).CreateCell(0).SetCellValue(radio == "FM 99.1" ? "Frecuencia: 99.1" : "Frecuencia: 100.3"); | ||||
|                 sheet.CreateRow(currentRowIdx++).CreateCell(0).SetCellValue($"Mes: {mesText}/{anioFullText}"); | ||||
|             } | ||||
|  | ||||
|             IRow headerDataRow = sheet.CreateRow(currentRowIdx++); | ||||
|             headerDataRow.CreateCell(0).SetCellValue("Día"); | ||||
|             headerDataRow.CreateCell(1).SetCellValue("Hora"); | ||||
|             headerDataRow.CreateCell(2).SetCellValue("Título de la Obra"); | ||||
|             headerDataRow.CreateCell(3).SetCellValue("Compositor-Autor"); | ||||
|             headerDataRow.CreateCell(4).SetCellValue("Intérprete"); | ||||
|             headerDataRow.CreateCell(5).SetCellValue("Sello"); | ||||
|             headerDataRow.CreateCell(6).SetCellValue("Álbum"); | ||||
|  | ||||
|             IFont font = workbook.CreateFont(); | ||||
|             font.FontHeightInPoints = 10; | ||||
|             font.FontName = "Arial"; | ||||
|             ICellStyle cellStyle = workbook.CreateCellStyle(); | ||||
|             cellStyle.SetFont(font); | ||||
|  | ||||
|             foreach (var item in programacion) | ||||
|             { | ||||
|                 IRow dataRow = sheet.CreateRow(currentRowIdx++); | ||||
|                 dataRow.CreateCell(0).SetCellValue(item.Dia); | ||||
|                 dataRow.CreateCell(1).SetCellValue(item.Hora); | ||||
|                 dataRow.CreateCell(2).SetCellValue(item.TituloObra); | ||||
|                 dataRow.CreateCell(3).SetCellValue(item.CompositorAutor); | ||||
|                 dataRow.CreateCell(4).SetCellValue(item.Interprete); | ||||
|                 dataRow.CreateCell(5).SetCellValue(item.Sello); | ||||
|                 dataRow.CreateCell(6).SetCellValue(item.Album); | ||||
|  | ||||
|                 for (int i = 0; i < 7; i++) | ||||
|                 { | ||||
|                     ICell cell = dataRow.GetCell(i) ?? dataRow.CreateCell(i); // Asegurarse que la celda exista | ||||
|                     cell.CellStyle = cellStyle; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             sheet.SetColumnWidth(0, 4 * 256); | ||||
|             sheet.SetColumnWidth(1, 5 * 256); | ||||
|             sheet.SetColumnWidth(2, 25 * 256); | ||||
|             sheet.SetColumnWidth(3, 14 * 256); | ||||
|             sheet.SetColumnWidth(4, 11 * 256); | ||||
|             sheet.SetColumnWidth(5, 11 * 256); | ||||
|             sheet.SetColumnWidth(6, 30 * 256); | ||||
|  | ||||
|             short rowHeight = 255; // 12.75 puntos | ||||
|             for (int i = 0; i < currentRowIdx; i++) | ||||
|             { | ||||
|                 IRow row = sheet.GetRow(i) ?? sheet.CreateRow(i); | ||||
|                 row.Height = rowHeight; | ||||
|  | ||||
|                 // Aplicar estilo a todas las celdas de las filas de encabezado también | ||||
|                 if (i < (institucion == "AADI" ? 5 : 0) || i == (institucion == "AADI" ? 5 : 0)) // Filas de cabecera de AADI o la fila de títulos de columnas | ||||
|                 { | ||||
|                     // Iterar sobre las celdas que realmente existen o deberían existir | ||||
|                     int lastCellNum = (institucion == "AADI" && i < 5) ? 1 : 7; // Cabecera AADI solo tiene 1 celda, títulos de datos tienen 7 | ||||
|                     for (int j = 0; j < lastCellNum; j++) | ||||
|                     { | ||||
|                         ICell cell = row.GetCell(j) ?? row.CreateCell(j); | ||||
|                         cell.CellStyle = cellStyle; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             using (var memoryStream = new MemoryStream()) | ||||
|             { | ||||
|                 workbook.Write(memoryStream, true); // El 'true' es para dejar el stream abierto si se necesita | ||||
|                 return memoryStream.ToArray(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										133
									
								
								Backend/GestionIntegral.Api/Services/Radios/RitmoService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								Backend/GestionIntegral.Api/Services/Radios/RitmoService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| using GestionIntegral.Api.Data.Repositories.Radios; | ||||
| using GestionIntegral.Api.Dtos.Radios; | ||||
| using GestionIntegral.Api.Models.Radios; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| // using GestionIntegral.Api.Data; // Para DbConnectionFactory si se usa transacción | ||||
| // using System.Data; // Para IsolationLevel si se usa transacción | ||||
|  | ||||
| namespace GestionIntegral.Api.Services.Radios | ||||
| { | ||||
|     public class RitmoService : IRitmoService | ||||
|     { | ||||
|         private readonly IRitmoRepository _ritmoRepository; | ||||
|         private readonly ILogger<RitmoService> _logger; | ||||
|         // private readonly DbConnectionFactory _connectionFactory; // Si se implementa historial | ||||
|  | ||||
|         public RitmoService(IRitmoRepository ritmoRepository, ILogger<RitmoService> logger /*, DbConnectionFactory cf */) | ||||
|         { | ||||
|             _ritmoRepository = ritmoRepository; | ||||
|             _logger = logger; | ||||
|             // _connectionFactory = cf; | ||||
|         } | ||||
|  | ||||
|         private RitmoDto MapToDto(Ritmo ritmo) => new RitmoDto | ||||
|         { | ||||
|             Id = ritmo.Id, | ||||
|             NombreRitmo = ritmo.NombreRitmo | ||||
|         }; | ||||
|  | ||||
|         public async Task<IEnumerable<RitmoDto>> ObtenerTodosAsync(string? nombreFilter) | ||||
|         { | ||||
|             var ritmos = await _ritmoRepository.GetAllAsync(nombreFilter); | ||||
|             return ritmos.Select(MapToDto); | ||||
|         } | ||||
|  | ||||
|         public async Task<RitmoDto?> ObtenerPorIdAsync(int id) | ||||
|         { | ||||
|             var ritmo = await _ritmoRepository.GetByIdAsync(id); | ||||
|             return ritmo == null ? null : MapToDto(ritmo); | ||||
|         } | ||||
|  | ||||
|         public async Task<(RitmoDto? Ritmo, string? Error)> CrearAsync(CreateRitmoDto createDto, int idUsuario) | ||||
|         { | ||||
|             if (await _ritmoRepository.ExistsByNameAsync(createDto.NombreRitmo)) | ||||
|             { | ||||
|                 return (null, "El nombre del ritmo ya existe."); | ||||
|             } | ||||
|  | ||||
|             var nuevoRitmo = new Ritmo { NombreRitmo = createDto.NombreRitmo }; | ||||
|  | ||||
|             // Sin historial, la transacción es opcional para una sola operación, | ||||
|             // pero se deja la estructura por si se añade más lógica o historial. | ||||
|             // 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 ritmoCreado = await _ritmoRepository.CreateAsync(nuevoRitmo /*, idUsuario, transaction */); | ||||
|                 if (ritmoCreado == null) return (null, "Error al crear el ritmo."); | ||||
|  | ||||
|                 // transaction.Commit(); | ||||
|                 _logger.LogInformation("Ritmo ID {Id} creado por Usuario ID {UserId}.", ritmoCreado.Id, idUsuario); | ||||
|                 return (MapToDto(ritmoCreado), null); | ||||
|             } | ||||
|             catch (System.Exception ex) | ||||
|             { | ||||
|                 // try { transaction.Rollback(); } catch {} | ||||
|                 _logger.LogError(ex, "Error CrearAsync Ritmo: {NombreRitmo}", createDto.NombreRitmo); | ||||
|                 return (null, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateRitmoDto updateDto, int idUsuario) | ||||
|         { | ||||
|             var ritmoExistente = await _ritmoRepository.GetByIdAsync(id); | ||||
|             if (ritmoExistente == null) return (false, "Ritmo no encontrado."); | ||||
|  | ||||
|             if (ritmoExistente.NombreRitmo != updateDto.NombreRitmo && await _ritmoRepository.ExistsByNameAsync(updateDto.NombreRitmo, id)) | ||||
|             { | ||||
|                 return (false, "El nombre del ritmo ya existe para otro registro."); | ||||
|             } | ||||
|  | ||||
|             ritmoExistente.NombreRitmo = updateDto.NombreRitmo; | ||||
|  | ||||
|             // Sin historial, la transacción es opcional... | ||||
|             try | ||||
|             { | ||||
|                 var actualizado = await _ritmoRepository.UpdateAsync(ritmoExistente /*, idUsuario, transaction */); | ||||
|                 if (!actualizado) return (false, "Error al actualizar el ritmo."); | ||||
|                  | ||||
|                 // transaction.Commit(); | ||||
|                 _logger.LogInformation("Ritmo ID {Id} actualizado por Usuario ID {UserId}.", id, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (System.Exception ex) | ||||
|             { | ||||
|                 // try { transaction.Rollback(); } catch {} | ||||
|                 _logger.LogError(ex, "Error ActualizarAsync Ritmo ID: {Id}", id); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) | ||||
|         { | ||||
|             var ritmoExistente = await _ritmoRepository.GetByIdAsync(id); | ||||
|             if (ritmoExistente == null) return (false, "Ritmo no encontrado."); | ||||
|  | ||||
|             if (await _ritmoRepository.IsInUseAsync(id)) | ||||
|             { | ||||
|                 return (false, "No se puede eliminar. El ritmo está asignado a una o más canciones."); | ||||
|             } | ||||
|  | ||||
|             // Sin historial, la transacción es opcional... | ||||
|             try | ||||
|             { | ||||
|                 var eliminado = await _ritmoRepository.DeleteAsync(id /*, idUsuario, transaction */); | ||||
|                 if (!eliminado) return (false, "Error al eliminar el ritmo."); | ||||
|  | ||||
|                 // transaction.Commit(); | ||||
|                 _logger.LogInformation("Ritmo ID {Id} eliminado por Usuario ID {UserId}.", id, idUsuario); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (System.Exception ex) | ||||
|             { | ||||
|                 // try { transaction.Rollback(); } catch {} | ||||
|                 _logger.LogError(ex, "Error EliminarAsync Ritmo ID: {Id}", id); | ||||
|                 return (false, $"Error interno: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| using GestionIntegral.Api.Dtos.Usuarios; | ||||
| using GestionIntegral.Api.Dtos.Usuarios.Auditoria; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| @@ -13,6 +14,7 @@ namespace GestionIntegral.Api.Services.Usuarios | ||||
|         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); | ||||
|  | ||||
|         Task<IEnumerable<UsuarioHistorialDto>> ObtenerHistorialPorUsuarioIdAsync(int idUsuarioAfectado, DateTime? fechaDesde, DateTime? fechaHasta); | ||||
|         Task<IEnumerable<UsuarioHistorialDto>> ObtenerTodoElHistorialAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModificoFilter, string? tipoModFilter); | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| using GestionIntegral.Api.Data; | ||||
| using GestionIntegral.Api.Data.Repositories.Usuarios; | ||||
| using GestionIntegral.Api.Dtos.Usuarios; | ||||
| using GestionIntegral.Api.Dtos.Usuarios.Auditoria; | ||||
| using GestionIntegral.Api.Models.Usuarios; // Para Usuario | ||||
| using Microsoft.Extensions.Logging; | ||||
| using System.Collections.Generic; | ||||
| @@ -79,7 +80,7 @@ namespace GestionIntegral.Api.Services.Usuarios | ||||
|             { | ||||
|                 return (null, "El perfil seleccionado no es válido."); | ||||
|             } | ||||
|             if(createDto.User.Equals(createDto.Password, System.StringComparison.OrdinalIgnoreCase)) | ||||
|             if (createDto.User.Equals(createDto.Password, System.StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 return (null, "La contraseña no puede ser igual al nombre de usuario."); | ||||
|             } | ||||
| @@ -112,11 +113,18 @@ namespace GestionIntegral.Api.Services.Usuarios | ||||
|                 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, | ||||
|                 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 | ||||
|                     DebeCambiarClave = usuarioCreado.DebeCambiarClave, | ||||
|                     VerLog = usuarioCreado.VerLog | ||||
|                 }; | ||||
|                 _logger.LogInformation("Usuario ID {UsuarioId} creado por Usuario ID {CreadorId}.", usuarioCreado.Id, idUsuarioCreador); | ||||
|                 return (dto, null); | ||||
| @@ -160,8 +168,9 @@ namespace GestionIntegral.Api.Services.Usuarios | ||||
|                 _logger.LogInformation("Usuario ID {UsuarioId} actualizado por Usuario ID {ModificadorId}.", id, idUsuarioModificador); | ||||
|                 return (true, null); | ||||
|             } | ||||
|             catch (KeyNotFoundException) { | ||||
|                  try { transaction.Rollback(); } catch { /* Log */ } | ||||
|             catch (KeyNotFoundException) | ||||
|             { | ||||
|                 try { transaction.Rollback(); } catch { /* Log */ } | ||||
|                 return (false, "Usuario no encontrado durante la actualización."); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
| @@ -176,9 +185,9 @@ namespace GestionIntegral.Api.Services.Usuarios | ||||
|             var usuario = await _usuarioRepository.GetByIdAsync(userId); | ||||
|             if (usuario == null) return (false, "Usuario no encontrado."); | ||||
|  | ||||
|             if(usuario.User.Equals(setPasswordDto.NewPassword, System.StringComparison.OrdinalIgnoreCase)) | ||||
|             if (usuario.User.Equals(setPasswordDto.NewPassword, System.StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                  return (false, "La nueva contraseña no puede ser igual al nombre de usuario."); | ||||
|                 return (false, "La nueva contraseña no puede ser igual al nombre de usuario."); | ||||
|             } | ||||
|  | ||||
|             (string hash, string salt) = _passwordHasher.HashPassword(setPasswordDto.NewPassword); | ||||
| @@ -189,14 +198,15 @@ namespace GestionIntegral.Api.Services.Usuarios | ||||
|             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."); | ||||
|                 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 */ } | ||||
|             catch (KeyNotFoundException) | ||||
|             { | ||||
|                 try { transaction.Rollback(); } catch { /* Log */ } | ||||
|                 return (false, "Usuario no encontrado durante el cambio de contraseña."); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
| @@ -207,7 +217,7 @@ namespace GestionIntegral.Api.Services.Usuarios | ||||
|             } | ||||
|         } | ||||
|  | ||||
|          public async Task<(bool Exito, string? Error)> CambiarEstadoHabilitadoAsync(int userId, bool habilitar, int idUsuarioModificador) | ||||
|         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."); | ||||
| @@ -225,22 +235,34 @@ namespace GestionIntegral.Api.Services.Usuarios | ||||
|             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."); | ||||
|                 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 */ } | ||||
|             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 */ } | ||||
|                 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}"); | ||||
|             } | ||||
|         } | ||||
|         public async Task<IEnumerable<UsuarioHistorialDto>> ObtenerHistorialPorUsuarioIdAsync(int idUsuarioAfectado, DateTime? fechaDesde, DateTime? fechaHasta) | ||||
|         { | ||||
|             // Aquí podrías añadir validaciones extra si fuera necesario antes de llamar al repo | ||||
|             return await _usuarioRepository.GetHistorialByUsuarioIdAsync(idUsuarioAfectado, fechaDesde, fechaHasta); | ||||
|         } | ||||
|  | ||||
|         public async Task<IEnumerable<UsuarioHistorialDto>> ObtenerTodoElHistorialAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModificoFilter, string? tipoModFilter) | ||||
|         { | ||||
|             // Aquí podrías añadir validaciones extra | ||||
|             return await _usuarioRepository.GetAllHistorialAsync(fechaDesde, fechaHasta, idUsuarioModificoFilter, tipoModFilter); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user