From 3c1fe15b1f03aef4b6b6b6482c8e9fa92dfbf7cd Mon Sep 17 00:00:00 2001 From: eldiadmolinari Date: Fri, 23 May 2025 15:47:39 -0300 Subject: [PATCH] =?UTF-8?q?Ya=20perd=C3=AD=20el=20hilo=20de=20los=20cambio?= =?UTF-8?q?s=20pero=20ahi=20van.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Contables/NotasCreditoDebitoController.cs | 130 +++++ .../Contables/PagosDistribuidorController.cs | 130 +++++ .../ControlDevolucionesController.cs | 130 +++++ .../EntradasSalidasCanillaController.cs | 182 ++++++ .../Controllers/Radios/CancionesController.cs | 124 ++++ .../Radios/RadioListasController.cs | 65 +++ .../Controllers/Radios/RitmosController.cs | 125 ++++ .../Usuarios/UsuariosController.cs | 50 +- .../Contables/INotaCreditoDebitoRepository.cs | 21 + .../Contables/IPagoDistribuidorRepository.cs | 21 + .../Contables/NotaCreditoDebitoRepository.cs | 153 +++++ .../Contables/PagoDistribuidorRepository.cs | 175 ++++++ .../ControlDevolucionesRepository.cs | 162 ++++++ .../EntradaSalidaCanillaRepository.cs | 298 ++++++++++ .../IControlDevolucionesRepository.cs | 18 + .../IEntradaSalidaCanillaRepository.cs | 23 + .../Repositories/Radios/CancionRepository.cs | 196 +++++++ .../Repositories/Radios/ICancionRepository.cs | 18 + .../Repositories/Radios/IRitmoRepository.cs | 18 + .../Repositories/Radios/RitmoRepository.cs | 149 +++++ .../Usuarios/IUsuarioRepository.cs | 5 +- .../Usuarios/UsuarioRepository.cs | 149 ++++- .../GestionIntegral.Api.csproj | 1 + .../Models/Contables/NotaCreditoDebito.cs | 16 + .../Contables/NotaCreditoDebitoHistorico.cs | 20 + .../Models/Contables/PagoDistribuidor.cs | 16 + .../Contables/PagoDistribuidorHistorico.cs | 20 + .../Distribucion/ControlDevoluciones.cs | 14 + .../ControlDevolucionesHistorico.cs | 18 + .../Distribucion/EntradaSalidaCanilla.cs | 20 + .../Models/Dtos/Contables/CreateNotaDto.cs | 34 ++ .../Contables/CreatePagoDistribuidorDto.cs | 25 + .../Dtos/Contables/NotaCreditoDebitoDto.cs | 17 + .../Dtos/Contables/PagoDistribuidorDto.cs | 18 + .../Models/Dtos/Contables/UpdateNotaDto.cs | 15 + .../Contables/UpdatePagoDistribuidorDto.cs | 17 + .../Distribucion/ControlDevolucionesDto.cs | 14 + .../CreateBulkEntradaSalidaCanillaDto.cs | 41 ++ .../CreateControlDevolucionesDto.cs | 20 + .../CreateEntradaSalidaCanillaDto.cs | 34 ++ .../Distribucion/EntradaSalidaCanillaDto.cs | 26 + .../EntradaSalidaCanillaItemDto.cs | 35 ++ .../LiquidarMovimientosCanillaDto.cs | 14 + .../UpdateControlDevolucionesDto.cs | 16 + .../UpdateEntradaSalidaCanillaDto.cs | 27 + .../Models/Dtos/Radios/CancionDto.cs | 18 + .../Models/Dtos/Radios/CancionEnListaDto.cs | 17 + .../Models/Dtos/Radios/CreateCancionDto.cs | 34 ++ .../Models/Dtos/Radios/CreateRitmoDto.cs | 12 + .../Radios/GenerarListaRadioRequestDto.cs | 23 + .../Radios/ProgramacionHorariaExcelDto.cs | 16 + .../Models/Dtos/Radios/RitmoDto.cs | 8 + .../Models/Dtos/Radios/UpdateCancionDto.cs | 34 ++ .../Models/Dtos/Radios/UpdateRitmoDto.cs | 10 + .../Usuarios/Auditoria/UsuarioHistorialDto.cs | 35 ++ .../Models/Radios/Cancion.cs | 17 + .../Models/Radios/Ritmo.cs | 8 + Backend/GestionIntegral.Api/Program.cs | 15 + .../Contables/INotaCreditoDebitoService.cs | 19 + .../Contables/IPagoDistribuidorService.cs | 19 + .../Contables/NotaCreditoDebitoService.cs | 226 ++++++++ .../Contables/PagoDistribuidorService.cs | 218 +++++++ .../ControlDevolucionesService.cs | 171 ++++++ .../EntradaSalidaCanillaService.cs | 481 +++++++++++++++ .../Distribucion/EntradaSalidaDistService.cs | 52 +- .../IControlDevolucionesService.cs | 16 + .../IEntradaSalidaCanillaService.cs | 23 + .../Services/Radios/CancionService.cs | 173 ++++++ .../Services/Radios/ICancionService.cs | 15 + .../Services/Radios/IRadioListaService.cs | 12 + .../Services/Radios/IRitmoService.cs | 15 + .../Services/Radios/RadioListaService.cs | 224 +++++++ .../Services/Radios/RitmoService.cs | 133 +++++ .../Services/Usuarios/IUsuarioService.cs | 4 +- .../Services/Usuarios/UsuarioService.cs | 56 +- .../net9.0/GestionIntegral.Api.deps.json | 196 +++++++ .../GestionIntegral.Api.AssemblyInfo.cs | 2 +- ...onIntegral.Api.csproj.FileListAbsolute.txt | 12 + .../Debug/net9.0/rjsmcshtml.dswa.cache.json | 2 +- .../Debug/net9.0/rjsmrazor.dswa.cache.json | 2 +- ...stionIntegral.Api.csproj.nuget.dgspec.json | 4 + .../obj/project.assets.json | 546 ++++++++++++++++++ .../Contables/NotaCreditoDebitoFormModal.tsx | 271 +++++++++ .../Contables/PagoDistribuidorFormModal.tsx | 248 ++++++++ .../Modals/Contables/TipoPagoFormModal.tsx | 2 +- .../ControlDevolucionesFormModal.tsx | 201 +++++++ .../EntradaSalidaCanillaFormModal.tsx | 396 +++++++++++++ .../EntradaSalidaDistFormModal.tsx | 17 +- .../Modals/Radios/CancionFormModal.tsx | 212 +++++++ .../Modals/Radios/RitmoFormModal.tsx | 111 ++++ Frontend/src/models/{ => Entities}/Empresa.ts | 0 .../models/dtos/Contables/CreateNotaDto.ts | 10 + .../Contables/CreatePagoDistribuidorDto.ts | 10 + .../CreateTipoPagoDto.ts | 0 .../dtos/Contables/NotaCreditoDebitoDto.ts | 13 + .../dtos/Contables/PagoDistribuidorDto.ts | 14 + .../models/dtos/Contables/UpdateNotaDto.ts | 4 + .../Contables/UpdatePagoDistribuidorDto.ts | 5 + .../UpdateTipoPagoDto.ts | 0 .../Distribucion/ControlDevolucionesDto.ts | 10 + .../CreateBulkEntradaSalidaCanillaDto.ts | 7 + .../CreateControlDevolucionesDto.ts | 8 + .../CreateEntradaSalidaCanillaDto.ts | 8 + .../Distribucion/EntradaSalidaCanillaDto.ts | 22 + .../EntradaSalidaCanillaItemDto.ts | 6 + .../LiquidarMovimientosCanillaDto.ts | 4 + .../UpdateControlDevolucionesDto.ts | 6 + .../UpdateEntradaSalidaCanillaDto.ts | 5 + Frontend/src/models/dtos/Radios/CancionDto.ts | 14 + .../models/dtos/Radios/CreateCancionDto.ts | 12 + .../src/models/dtos/Radios/CreateRitmoDto.ts | 3 + .../Radios/GenerarListaRadioRequestDto.ts | 6 + Frontend/src/models/dtos/Radios/RitmoDto.ts | 4 + .../models/dtos/Radios/UpdateCancionDto.ts | 12 + .../src/models/dtos/Radios/UpdateRitmoDto.ts | 3 + .../Usuarios/Auditoria/UsuarioHistorialDto.ts | 27 + .../pages/Contables/ContablesIndexPage.tsx | 4 +- .../pages/Contables/GestionarNotasCDPage.tsx | 262 +++++++++ .../GestionarPagosDistribuidorPage.tsx | 231 ++++++++ .../Contables/GestionarTiposPagoPage.tsx | 4 +- .../src/pages/Distribucion/ESCanillasPage.tsx | 7 - .../GestionarControlDevolucionesPage.tsx | 220 +++++++ .../GestionarEntradasSalidasCanillaPage.tsx | 369 ++++++++++++ .../GestionarEntradasSalidasDistPage.tsx | 215 ++++--- .../pages/Radios/GenerarListasRadioPage.tsx | 210 +++++++ .../pages/Radios/GestionarCancionesPage.tsx | 195 +++++++ .../src/pages/Radios/GestionarRitmosPage.tsx | 164 ++++++ Frontend/src/pages/Radios/RadiosIndexPage.tsx | 56 ++ .../GestionarAuditoriaUsuariosPage.tsx | 256 ++++++++ .../src/pages/Usuarios/UsuariosIndexPage.tsx | 1 + Frontend/src/routes/AppRoutes.tsx | 32 +- .../Contables/notaCreditoDebitoService.ts | 54 ++ .../Contables/pagoDistribuidorService.ts | 52 ++ .../src/services/Contables/tipoPagoService.ts | 4 +- .../controlDevolucionesService.ts | 48 ++ .../entradaSalidaCanillaService.ts | 72 +++ .../Distribucion/entradaSalidaDistService.ts | 2 +- .../src/services/Radios/cancionService.ts | 48 ++ .../src/services/Radios/radioListaService.ts | 20 + Frontend/src/services/Radios/ritmoService.ts | 40 ++ .../src/services/Usuarios/usuarioService.ts | 29 + 141 files changed, 9764 insertions(+), 190 deletions(-) create mode 100644 Backend/GestionIntegral.Api/Controllers/Contables/NotasCreditoDebitoController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Contables/PagosDistribuidorController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Distribucion/ControlDevolucionesController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Distribucion/EntradasSalidasCanillaController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Radios/CancionesController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Radios/RadioListasController.cs create mode 100644 Backend/GestionIntegral.Api/Controllers/Radios/RitmosController.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Contables/INotaCreditoDebitoRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Contables/IPagoDistribuidorRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Contables/NotaCreditoDebitoRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Contables/PagoDistribuidorRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ControlDevolucionesRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EntradaSalidaCanillaRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IControlDevolucionesRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEntradaSalidaCanillaRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Radios/CancionRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Radios/ICancionRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Radios/IRitmoRepository.cs create mode 100644 Backend/GestionIntegral.Api/Data/Repositories/Radios/RitmoRepository.cs create mode 100644 Backend/GestionIntegral.Api/Models/Contables/NotaCreditoDebito.cs create mode 100644 Backend/GestionIntegral.Api/Models/Contables/NotaCreditoDebitoHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Contables/PagoDistribuidor.cs create mode 100644 Backend/GestionIntegral.Api/Models/Contables/PagoDistribuidorHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/ControlDevoluciones.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/ControlDevolucionesHistorico.cs create mode 100644 Backend/GestionIntegral.Api/Models/Distribucion/EntradaSalidaCanilla.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Contables/CreateNotaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Contables/CreatePagoDistribuidorDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Contables/NotaCreditoDebitoDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Contables/PagoDistribuidorDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Contables/UpdateNotaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Contables/UpdatePagoDistribuidorDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/ControlDevolucionesDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateControlDevolucionesDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateEntradaSalidaCanillaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EntradaSalidaCanillaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EntradaSalidaCanillaItemDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/LiquidarMovimientosCanillaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateControlDevolucionesDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateEntradaSalidaCanillaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Radios/CancionDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Radios/CancionEnListaDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Radios/CreateCancionDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Radios/CreateRitmoDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Radios/GenerarListaRadioRequestDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Radios/ProgramacionHorariaExcelDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Radios/RitmoDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Radios/UpdateCancionDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Radios/UpdateRitmoDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Dtos/Usuarios/Auditoria/UsuarioHistorialDto.cs create mode 100644 Backend/GestionIntegral.Api/Models/Radios/Cancion.cs create mode 100644 Backend/GestionIntegral.Api/Models/Radios/Ritmo.cs create mode 100644 Backend/GestionIntegral.Api/Services/Contables/INotaCreditoDebitoService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Contables/IPagoDistribuidorService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Contables/NotaCreditoDebitoService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/ControlDevolucionesService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/EntradaSalidaCanillaService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/IControlDevolucionesService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Distribucion/IEntradaSalidaCanillaService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Radios/CancionService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Radios/ICancionService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Radios/IRadioListaService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Radios/IRitmoService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Radios/RadioListaService.cs create mode 100644 Backend/GestionIntegral.Api/Services/Radios/RitmoService.cs create mode 100644 Frontend/src/components/Modals/Contables/NotaCreditoDebitoFormModal.tsx create mode 100644 Frontend/src/components/Modals/Contables/PagoDistribuidorFormModal.tsx create mode 100644 Frontend/src/components/Modals/Distribucion/ControlDevolucionesFormModal.tsx create mode 100644 Frontend/src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx create mode 100644 Frontend/src/components/Modals/Radios/CancionFormModal.tsx create mode 100644 Frontend/src/components/Modals/Radios/RitmoFormModal.tsx rename Frontend/src/models/{ => Entities}/Empresa.ts (100%) create mode 100644 Frontend/src/models/dtos/Contables/CreateNotaDto.ts create mode 100644 Frontend/src/models/dtos/Contables/CreatePagoDistribuidorDto.ts rename Frontend/src/models/dtos/{tiposPago => Contables}/CreateTipoPagoDto.ts (100%) create mode 100644 Frontend/src/models/dtos/Contables/NotaCreditoDebitoDto.ts create mode 100644 Frontend/src/models/dtos/Contables/PagoDistribuidorDto.ts create mode 100644 Frontend/src/models/dtos/Contables/UpdateNotaDto.ts create mode 100644 Frontend/src/models/dtos/Contables/UpdatePagoDistribuidorDto.ts rename Frontend/src/models/dtos/{tiposPago => Contables}/UpdateTipoPagoDto.ts (100%) create mode 100644 Frontend/src/models/dtos/Distribucion/ControlDevolucionesDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/CreateControlDevolucionesDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/CreateEntradaSalidaCanillaDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/EntradaSalidaCanillaDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/EntradaSalidaCanillaItemDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/LiquidarMovimientosCanillaDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/UpdateControlDevolucionesDto.ts create mode 100644 Frontend/src/models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto.ts create mode 100644 Frontend/src/models/dtos/Radios/CancionDto.ts create mode 100644 Frontend/src/models/dtos/Radios/CreateCancionDto.ts create mode 100644 Frontend/src/models/dtos/Radios/CreateRitmoDto.ts create mode 100644 Frontend/src/models/dtos/Radios/GenerarListaRadioRequestDto.ts create mode 100644 Frontend/src/models/dtos/Radios/RitmoDto.ts create mode 100644 Frontend/src/models/dtos/Radios/UpdateCancionDto.ts create mode 100644 Frontend/src/models/dtos/Radios/UpdateRitmoDto.ts create mode 100644 Frontend/src/models/dtos/Usuarios/Auditoria/UsuarioHistorialDto.ts create mode 100644 Frontend/src/pages/Contables/GestionarNotasCDPage.tsx create mode 100644 Frontend/src/pages/Contables/GestionarPagosDistribuidorPage.tsx delete mode 100644 Frontend/src/pages/Distribucion/ESCanillasPage.tsx create mode 100644 Frontend/src/pages/Distribucion/GestionarControlDevolucionesPage.tsx create mode 100644 Frontend/src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx create mode 100644 Frontend/src/pages/Radios/GenerarListasRadioPage.tsx create mode 100644 Frontend/src/pages/Radios/GestionarCancionesPage.tsx create mode 100644 Frontend/src/pages/Radios/GestionarRitmosPage.tsx create mode 100644 Frontend/src/pages/Radios/RadiosIndexPage.tsx create mode 100644 Frontend/src/pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage.tsx create mode 100644 Frontend/src/services/Contables/notaCreditoDebitoService.ts create mode 100644 Frontend/src/services/Contables/pagoDistribuidorService.ts create mode 100644 Frontend/src/services/Distribucion/controlDevolucionesService.ts create mode 100644 Frontend/src/services/Distribucion/entradaSalidaCanillaService.ts create mode 100644 Frontend/src/services/Radios/cancionService.ts create mode 100644 Frontend/src/services/Radios/radioListaService.ts create mode 100644 Frontend/src/services/Radios/ritmoService.ts diff --git a/Backend/GestionIntegral.Api/Controllers/Contables/NotasCreditoDebitoController.cs b/Backend/GestionIntegral.Api/Controllers/Contables/NotasCreditoDebitoController.cs new file mode 100644 index 0000000..3d0f3be --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Contables/NotasCreditoDebitoController.cs @@ -0,0 +1,130 @@ +using GestionIntegral.Api.Dtos.Contables; +using GestionIntegral.Api.Services.Contables; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Contables +{ + [Route("api/notascreditodebito")] // Ruta base + [ApiController] + [Authorize] + public class NotasCreditoDebitoController : ControllerBase + { + private readonly INotaCreditoDebitoService _notaService; + private readonly ILogger _logger; + + // Permisos para Notas C/D (CN001 a CN004) + private const string PermisoVer = "CN001"; + private const string PermisoCrear = "CN002"; + private const string PermisoModificar = "CN003"; + private const string PermisoEliminar = "CN004"; + + public NotasCreditoDebitoController(INotaCreditoDebitoService notaService, ILogger logger) + { + _notaService = notaService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en NotasCreditoDebitoController."); + return null; + } + + // GET: api/notascreditodebito + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAll( + [FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta, + [FromQuery] string? destino, [FromQuery] int? idDestino, + [FromQuery] int? idEmpresa, [FromQuery] string? tipoNota) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var notas = await _notaService.ObtenerTodosAsync(fechaDesde, fechaHasta, destino, idDestino, idEmpresa, tipoNota); + return Ok(notas); + } + + // GET: api/notascreditodebito/{idNota} + [HttpGet("{idNota:int}", Name = "GetNotaCreditoDebitoById")] + [ProducesResponseType(typeof(NotaCreditoDebitoDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetById(int idNota) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var nota = await _notaService.ObtenerPorIdAsync(idNota); + if (nota == null) return NotFound(new { message = $"Nota con ID {idNota} no encontrada." }); + return Ok(nota); + } + + // POST: api/notascreditodebito + [HttpPost] + [ProducesResponseType(typeof(NotaCreditoDebitoDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task Create([FromBody] CreateNotaDto createDto) + { + if (!TienePermiso(PermisoCrear)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _notaService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al registrar la nota."); + + return CreatedAtRoute("GetNotaCreditoDebitoById", new { idNota = dto.IdNota }, dto); + } + + // PUT: api/notascreditodebito/{idNota} + [HttpPut("{idNota:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Update(int idNota, [FromBody] UpdateNotaDto updateDto) + { + if (!TienePermiso(PermisoModificar)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _notaService.ActualizarAsync(idNota, updateDto, userId.Value); + if (!exito) + { + if (error == "Nota no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // DELETE: api/notascreditodebito/{idNota} + [HttpDelete("{idNota:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete(int idNota) + { + if (!TienePermiso(PermisoEliminar)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _notaService.EliminarAsync(idNota, userId.Value); + if (!exito) + { + if (error == "Nota no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Contables/PagosDistribuidorController.cs b/Backend/GestionIntegral.Api/Controllers/Contables/PagosDistribuidorController.cs new file mode 100644 index 0000000..2d07875 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Contables/PagosDistribuidorController.cs @@ -0,0 +1,130 @@ +using GestionIntegral.Api.Dtos.Contables; +using GestionIntegral.Api.Services.Contables; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Contables +{ + [Route("api/pagosdistribuidor")] // Ruta base + [ApiController] + [Authorize] + public class PagosDistribuidorController : ControllerBase + { + private readonly IPagoDistribuidorService _pagoService; + private readonly ILogger _logger; + + // Permisos para Pagos Distribuidores (CP001 a CP004) + private const string PermisoVer = "CP001"; + private const string PermisoCrear = "CP002"; + private const string PermisoModificar = "CP003"; + private const string PermisoEliminar = "CP004"; + + public PagosDistribuidorController(IPagoDistribuidorService pagoService, ILogger logger) + { + _pagoService = pagoService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en PagosDistribuidorController."); + return null; + } + + // GET: api/pagosdistribuidor + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAll( + [FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta, + [FromQuery] int? idDistribuidor, [FromQuery] int? idEmpresa, + [FromQuery] string? tipoMovimiento) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var pagos = await _pagoService.ObtenerTodosAsync(fechaDesde, fechaHasta, idDistribuidor, idEmpresa, tipoMovimiento); + return Ok(pagos); + } + + // GET: api/pagosdistribuidor/{idPago} + [HttpGet("{idPago:int}", Name = "GetPagoDistribuidorById")] + [ProducesResponseType(typeof(PagoDistribuidorDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetById(int idPago) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var pago = await _pagoService.ObtenerPorIdAsync(idPago); + if (pago == null) return NotFound(new { message = $"Pago con ID {idPago} no encontrado." }); + return Ok(pago); + } + + // POST: api/pagosdistribuidor + [HttpPost] + [ProducesResponseType(typeof(PagoDistribuidorDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task Create([FromBody] CreatePagoDistribuidorDto createDto) + { + if (!TienePermiso(PermisoCrear)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _pagoService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al registrar el pago."); + + return CreatedAtRoute("GetPagoDistribuidorById", new { idPago = dto.IdPago }, dto); + } + + // PUT: api/pagosdistribuidor/{idPago} + [HttpPut("{idPago:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Update(int idPago, [FromBody] UpdatePagoDistribuidorDto updateDto) + { + if (!TienePermiso(PermisoModificar)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _pagoService.ActualizarAsync(idPago, updateDto, userId.Value); + if (!exito) + { + if (error == "Pago no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // DELETE: api/pagosdistribuidor/{idPago} + [HttpDelete("{idPago:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete(int idPago) + { + if (!TienePermiso(PermisoEliminar)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _pagoService.EliminarAsync(idPago, userId.Value); + if (!exito) + { + if (error == "Pago no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/ControlDevolucionesController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/ControlDevolucionesController.cs new file mode 100644 index 0000000..f24f79e --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/ControlDevolucionesController.cs @@ -0,0 +1,130 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/controldevoluciones")] // Ruta base + [ApiController] + [Authorize] + public class ControlDevolucionesController : ControllerBase + { + private readonly IControlDevolucionesService _controlDevService; + private readonly ILogger _logger; + + // Permisos para Control Devoluciones (CD001 a CD003) + private const string PermisoVer = "CD001"; + private const string PermisoCrear = "CD002"; + private const string PermisoModificar = "CD003"; + // Asumo que no hay un permiso específico para eliminar, se podría usar CD003 o crear CD004 + + public ControlDevolucionesController(IControlDevolucionesService controlDevService, ILogger logger) + { + _controlDevService = controlDevService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en ControlDevolucionesController."); + return null; + } + + // GET: api/controldevoluciones + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAll( + [FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta, + [FromQuery] int? idEmpresa) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var controles = await _controlDevService.ObtenerTodosAsync(fechaDesde, fechaHasta, idEmpresa); + return Ok(controles); + } + + // GET: api/controldevoluciones/{idControl} + [HttpGet("{idControl:int}", Name = "GetControlDevolucionesById")] + [ProducesResponseType(typeof(ControlDevolucionesDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetById(int idControl) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + var control = await _controlDevService.ObtenerPorIdAsync(idControl); + if (control == null) return NotFound(new { message = $"Control de devoluciones con ID {idControl} no encontrado." }); + return Ok(control); + } + + // POST: api/controldevoluciones + [HttpPost] + [ProducesResponseType(typeof(ControlDevolucionesDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task Create([FromBody] CreateControlDevolucionesDto createDto) + { + if (!TienePermiso(PermisoCrear)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _controlDevService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al registrar el control."); + + return CreatedAtRoute("GetControlDevolucionesById", new { idControl = dto.IdControl }, dto); + } + + // PUT: api/controldevoluciones/{idControl} + [HttpPut("{idControl:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Update(int idControl, [FromBody] UpdateControlDevolucionesDto updateDto) + { + if (!TienePermiso(PermisoModificar)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _controlDevService.ActualizarAsync(idControl, updateDto, userId.Value); + if (!exito) + { + if (error == "Control de devoluciones no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // DELETE: api/controldevoluciones/{idControl} + [HttpDelete("{idControl:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] // Podría usar PermisoModificar o un permiso de borrado específico + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete(int idControl) + { + // Asumimos que CD003 (Modificar) también permite eliminar, o se define un permiso específico + if (!TienePermiso(PermisoModificar)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _controlDevService.EliminarAsync(idControl, userId.Value); + if (!exito) + { + if (error == "Control de devoluciones no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/EntradasSalidasCanillaController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/EntradasSalidasCanillaController.cs new file mode 100644 index 0000000..e3d40c7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/EntradasSalidasCanillaController.cs @@ -0,0 +1,182 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using GestionIntegral.Api.Services.Distribucion; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/entradassalidascanilla")] // Ruta base + [ApiController] + [Authorize] + public class EntradasSalidasCanillaController : ControllerBase + { + private readonly IEntradaSalidaCanillaService _esCanillaService; + private readonly ILogger _logger; + + // Permisos para E/S Canillitas (MC001 a MC005) + private const string PermisoVerMovimientos = "MC001"; + private const string PermisoCrearMovimiento = "MC002"; + private const string PermisoModificarMovimiento = "MC003"; + private const string PermisoEliminarMovimiento = "MC004"; // (si no está liquidado) + private const string PermisoLiquidar = "MC005"; // Asumo que ver comprobante implica poder liquidar. + + public EntradasSalidasCanillaController(IEntradaSalidaCanillaService esCanillaService, ILogger logger) + { + _esCanillaService = esCanillaService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en EntradasSalidasCanillaController."); + return null; + } + + // GET: api/entradassalidascanilla + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAll( + [FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta, + [FromQuery] int? idPublicacion, [FromQuery] int? idCanilla, + [FromQuery] bool? liquidados, [FromQuery] bool? incluirNoLiquidados = true) // incluirNoLiquidados por defecto true para ver todo + { + if (!TienePermiso(PermisoVerMovimientos)) return Forbid(); + try + { + var movimientos = await _esCanillaService.ObtenerTodosAsync(fechaDesde, fechaHasta, idPublicacion, idCanilla, liquidados, incluirNoLiquidados); + return Ok(movimientos); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener listado de E/S Canillitas."); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno."); + } + } + + // GET: api/entradassalidascanilla/{idParte} + [HttpGet("{idParte:int}", Name = "GetEntradaSalidaCanillaById")] + [ProducesResponseType(typeof(EntradaSalidaCanillaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetById(int idParte) + { + if (!TienePermiso(PermisoVerMovimientos)) return Forbid(); + var movimiento = await _esCanillaService.ObtenerPorIdAsync(idParte); + if (movimiento == null) return NotFound(new { message = $"Movimiento con ID {idParte} no encontrado." }); + return Ok(movimiento); + } + + // PUT: api/entradassalidascanilla/{idParte} + [HttpPut("{idParte:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateMovimiento(int idParte, [FromBody] UpdateEntradaSalidaCanillaDto updateDto) + { + if (!TienePermiso(PermisoModificarMovimiento)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _esCanillaService.ActualizarMovimientoAsync(idParte, updateDto, userId.Value); + if (!exito) + { + if (error == "Movimiento no encontrado." || error == "No se puede modificar un movimiento ya liquidado.") + return NotFound(new { message = error }); // Podría ser 404 o 400 dependiendo del error + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // DELETE: api/entradassalidascanilla/{idParte} + [HttpDelete("{idParte:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteMovimiento(int idParte) + { + // Permiso base MC004 + // El servicio ahora usa User (ClaimsPrincipal) para verificar MC006 si es necesario + string permisoBaseRequerido = "MC004"; // Asumiendo que esta constante existe o la defines + + if (!TienePermiso(permisoBaseRequerido)) + { + _logger.LogWarning("Acceso denegado a DeleteMovimiento (IDParte: {IdParte}) para Usuario ID {UserId}. Permiso {Permiso} requerido.", idParte, GetCurrentUserId() ?? 0, permisoBaseRequerido); + return Forbid(); + } + + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + var (exito, error) = await _esCanillaService.EliminarMovimientoAsync(idParte, userId.Value, User); // Pasar ClaimsPrincipal (User) + + if (!exito) + { + if (error == "Movimiento no encontrado.") return NotFound(new { message = error }); + // Otros errores como "no tiene permiso para eliminar liquidados" o errores internos. + // El servicio podría devolver un código de estado más específico o el controlador interpretarlo. + // Por ahora, un BadRequest es genérico para fallos de lógica de negocio o permisos no cumplidos en el servicio. + if (error != null && error.Contains("No tiene permiso")) + { + _logger.LogWarning("Intento fallido de eliminar movimiento ID {IdParte} por Usuario ID {UserId}. Razón: {Error}", idParte, userId, error); + return StatusCode(StatusCodes.Status403Forbidden, new { message = error }); + } + return BadRequest(new { message = error ?? "Error desconocido al eliminar." }); + } + return NoContent(); + } + + // POST: api/entradassalidascanilla/liquidar + [HttpPost("liquidar")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task LiquidarMovimientos([FromBody] LiquidarMovimientosCanillaRequestDto liquidarDto) + { + if (!TienePermiso(PermisoLiquidar)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _esCanillaService.LiquidarMovimientosAsync(liquidarDto, userId.Value); + if (!exito) + { + return BadRequest(new { message = error }); + } + return NoContent(); + } + + [HttpPost("bulk")] // Nueva ruta o ajustar la existente y diferenciar por DTO + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreateBulkMovimientos([FromBody] CreateBulkEntradaSalidaCanillaDto createBulkDto) + { + // Mantener PermisoCrearMovimiento = "MC002" + if (!TienePermiso(PermisoCrearMovimiento)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dtos, error) = await _esCanillaService.CrearMovimientosEnLoteAsync(createBulkDto, userId.Value); + + if (error != null) return BadRequest(new { message = error }); + if (dtos == null || !dtos.Any()) return StatusCode(StatusCodes.Status500InternalServerError, "No se pudo registrar ningún movimiento o hubo un error."); + + // Podrías devolver solo un 201 Created si la lista de DTOs es muy grande + // o si no necesitas los detalles de cada uno inmediatamente. + // O devolver la lista como se hace aquí. + return StatusCode(StatusCodes.Status201Created, dtos); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Radios/CancionesController.cs b/Backend/GestionIntegral.Api/Controllers/Radios/CancionesController.cs new file mode 100644 index 0000000..2e079fb --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Radios/CancionesController.cs @@ -0,0 +1,124 @@ +using GestionIntegral.Api.Dtos.Radios; +using GestionIntegral.Api.Services.Radios; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Radios +{ + [Route("api/[controller]")] // Ruta base: /api/canciones + [ApiController] + [Authorize] + public class CancionesController : ControllerBase + { + private readonly ICancionService _cancionService; + private readonly ILogger _logger; + + // Asumir permisos para Canciones (ej. RC001-RC004 o usar SS005) + private const string PermisoVerCanciones = "SS005"; + private const string PermisoGestionarCanciones = "SS005"; + + public CancionesController(ICancionService cancionService, ILogger logger) + { + _cancionService = cancionService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en CancionesController."); + return null; + } + + // GET: api/canciones + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAll([FromQuery] string? tema, [FromQuery] string? interprete, [FromQuery] int? idRitmo) + { + if (!TienePermiso(PermisoVerCanciones)) return Forbid(); + var canciones = await _cancionService.ObtenerTodasAsync(tema, interprete, idRitmo); + return Ok(canciones); + } + + // GET: api/canciones/{id} + [HttpGet("{id:int}", Name = "GetCancionById")] + [ProducesResponseType(typeof(CancionDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetById(int id) + { + if (!TienePermiso(PermisoVerCanciones)) return Forbid(); + var cancion = await _cancionService.ObtenerPorIdAsync(id); + if (cancion == null) return NotFound(new { message = $"Canción con ID {id} no encontrada." }); + return Ok(cancion); + } + + // POST: api/canciones + [HttpPost] + [ProducesResponseType(typeof(CancionDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task Create([FromBody] CreateCancionDto createDto) + { + if (!TienePermiso(PermisoGestionarCanciones)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (dto, error) = await _cancionService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear la canción."); + + return CreatedAtRoute("GetCancionById", new { id = dto.Id }, dto); + } + + // PUT: api/canciones/{id} + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Update(int id, [FromBody] UpdateCancionDto updateDto) + { + if (!TienePermiso(PermisoGestionarCanciones)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _cancionService.ActualizarAsync(id, updateDto, userId.Value); + if (!exito) + { + if (error == "Canción no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // DELETE: api/canciones/{id} + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete(int id) + { + if (!TienePermiso(PermisoGestionarCanciones)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _cancionService.EliminarAsync(id, userId.Value); + if (!exito) + { + if (error == "Canción no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Radios/RadioListasController.cs b/Backend/GestionIntegral.Api/Controllers/Radios/RadioListasController.cs new file mode 100644 index 0000000..c2ee0ed --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Radios/RadioListasController.cs @@ -0,0 +1,65 @@ +using GestionIntegral.Api.Dtos.Radios; +using GestionIntegral.Api.Services.Radios; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Radios +{ + [Route("api/radios/listas")] // Ruta base, ej: /api/radios/listas + [ApiController] + [Authorize] + public class RadioListasController : ControllerBase + { + private readonly IRadioListaService _radioListaService; + private readonly ILogger _logger; + + // Asumir permiso general de Radios o uno específico para generar listas + private const string PermisoGenerarListas = "SS005"; // Usando el permiso general de la sección Radios + + public RadioListasController(IRadioListaService radioListaService, ILogger logger) + { + _radioListaService = radioListaService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + // GetCurrentUserId no es estrictamente necesario aquí si la acción no modifica datos persistentes auditables por usuario. + + // POST: api/radios/listas/generar + [HttpPost("generar")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] // Devuelve un archivo + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GenerarListaRadio([FromBody] GenerarListaRadioRequestDto requestDto) + { + if (!TienePermiso(PermisoGenerarListas)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + + _logger.LogInformation("Solicitud de generación de lista de radio recibida: {@RequestDto}", requestDto); + + var (fileContents, contentType, fileName, error) = await _radioListaService.GenerarListaRadioAsync(requestDto); + + if (error != null) + { + _logger.LogWarning("Error al generar lista de radio: {Error}", error); + // Devolver un JSON con el error podría ser más útil para el frontend que un simple BadRequest + return BadRequest(new { message = error }); + } + + if (fileContents == null || fileContents.Length == 0) + { + _logger.LogWarning("La generación de la lista de radio no produjo contenido."); + // Similar al anterior, un JSON con mensaje puede ser mejor + return NotFound(new { message = "No se pudo generar la lista, o no hay datos suficientes." }); + } + + _logger.LogInformation("Lista de radio generada exitosamente: {FileName}", fileName); + // Devuelve el archivo ZIP para descarga + return File(fileContents, contentType, fileName); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Radios/RitmosController.cs b/Backend/GestionIntegral.Api/Controllers/Radios/RitmosController.cs new file mode 100644 index 0000000..37ad71d --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Radios/RitmosController.cs @@ -0,0 +1,125 @@ +using GestionIntegral.Api.Dtos.Radios; +using GestionIntegral.Api.Services.Radios; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Controllers.Radios +{ + [Route("api/[controller]")] // Ruta base: /api/ritmos + [ApiController] + [Authorize] // Proteger todos los endpoints + public class RitmosController : ControllerBase + { + private readonly IRitmoService _ritmoService; + private readonly ILogger _logger; + + // Asumir códigos de permiso para Ritmos (ej. RR001-RR004) + // O usar permisos más genéricos de "Gestión Radios" si no hay específicos + private const string PermisoVerRitmos = "SS005"; // Usando el de acceso a la sección radios por ahora + private const string PermisoGestionarRitmos = "SS005"; // Idem para crear/mod/elim + + public RitmosController(IRitmoService ritmoService, ILogger logger) + { + _ritmoService = ritmoService; + _logger = logger; + } + + private bool TienePermiso(string codAcc) => User.IsInRole("SuperAdmin") || User.HasClaim(c => c.Type == "permission" && c.Value == codAcc); + private int? GetCurrentUserId() + { + if (int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"), out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en RitmosController."); + return null; + } + + // GET: api/ritmos + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAll([FromQuery] string? nombre) + { + if (!TienePermiso(PermisoVerRitmos)) return Forbid(); + var ritmos = await _ritmoService.ObtenerTodosAsync(nombre); + return Ok(ritmos); + } + + // GET: api/ritmos/{id} + [HttpGet("{id:int}", Name = "GetRitmoById")] + [ProducesResponseType(typeof(RitmoDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetById(int id) + { + if (!TienePermiso(PermisoVerRitmos)) return Forbid(); + var ritmo = await _ritmoService.ObtenerPorIdAsync(id); + if (ritmo == null) return NotFound(new { message = $"Ritmo con ID {id} no encontrado." }); + return Ok(ritmo); + } + + // POST: api/ritmos + [HttpPost] + [ProducesResponseType(typeof(RitmoDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task Create([FromBody] CreateRitmoDto createDto) + { + if (!TienePermiso(PermisoGestionarRitmos)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); // Aunque no se use en el repo sin historial, es bueno tenerlo + if (userId == null) return Unauthorized(); + + var (dto, error) = await _ritmoService.CrearAsync(createDto, userId.Value); + if (error != null) return BadRequest(new { message = error }); + if (dto == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear el ritmo."); + + return CreatedAtRoute("GetRitmoById", new { id = dto.Id }, dto); + } + + // PUT: api/ritmos/{id} + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Update(int id, [FromBody] UpdateRitmoDto updateDto) + { + if (!TienePermiso(PermisoGestionarRitmos)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _ritmoService.ActualizarAsync(id, updateDto, userId.Value); + if (!exito) + { + if (error == "Ritmo no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + + // DELETE: api/ritmos/{id} + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] // Si está en uso + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Delete(int id) + { + if (!TienePermiso(PermisoGestionarRitmos)) return Forbid(); + var userId = GetCurrentUserId(); + if (userId == null) return Unauthorized(); + + var (exito, error) = await _ritmoService.EliminarAsync(id, userId.Value); + if (!exito) + { + if (error == "Ritmo no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); // Ej: "En uso" + } + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Usuarios/UsuariosController.cs b/Backend/GestionIntegral.Api/Controllers/Usuarios/UsuariosController.cs index 4ef0808..4064f8c 100644 --- a/Backend/GestionIntegral.Api/Controllers/Usuarios/UsuariosController.cs +++ b/Backend/GestionIntegral.Api/Controllers/Usuarios/UsuariosController.cs @@ -1,4 +1,5 @@ using GestionIntegral.Api.Dtos.Usuarios; +using GestionIntegral.Api.Dtos.Usuarios.Auditoria; using GestionIntegral.Api.Services.Usuarios; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -141,18 +142,63 @@ namespace GestionIntegral.Api.Controllers.Usuarios [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task ToggleHabilitado(int id, [FromBody] bool habilitar) { - if (!TienePermiso(PermisoModificarUsuarios)) return Forbid(); + if (!TienePermiso(PermisoModificarUsuarios)) return Forbid(); var idAdmin = GetCurrentUserId(); if (idAdmin == null) return Unauthorized("Token inválido."); var (exito, error) = await _usuarioService.CambiarEstadoHabilitadoAsync(id, habilitar, idAdmin.Value); - if (!exito) + if (!exito) { if (error == "Usuario no encontrado.") return NotFound(new { message = error }); return BadRequest(new { message = error }); } return NoContent(); } + + // GET: api/usuarios/{idUsuarioAfectado}/historial + [HttpGet("{idUsuarioAfectado:int}/historial")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetHistorialDeUsuario(int idUsuarioAfectado, [FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta) + { + // Necesitas un permiso para ver historial, ej: "AU001" o uno más granular "AU_USR_VIEW_SINGLE" + // O si solo SuperAdmin puede ver historiales específicos. + if (!TienePermiso("AU001")) // Asumiendo AU001 = Ver Historial de Auditoría (General o Usuarios) + { + _logger.LogWarning("Acceso denegado a GetHistorialDeUsuario para Usuario ID {UserId}", GetCurrentUserId() ?? 0); + return Forbid(); + } + + var usuarioExiste = await _usuarioService.ObtenerPorIdAsync(idUsuarioAfectado); + if (usuarioExiste == null) + { + return NotFound(new { message = $"Usuario con ID {idUsuarioAfectado} no encontrado." }); + } + + var historial = await _usuarioService.ObtenerHistorialPorUsuarioIdAsync(idUsuarioAfectado, fechaDesde, fechaHasta); + return Ok(historial); + } + + // GET: api/usuarios/historial (Para todos los usuarios, con filtros) + [HttpGet("historial")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetTodoElHistorialDeUsuarios( + [FromQuery] DateTime? fechaDesde, + [FromQuery] DateTime? fechaHasta, + [FromQuery] int? idUsuarioModifico, + [FromQuery] string? tipoModificacion) + { + if (!TienePermiso("AU001")) // Mismo permiso general de auditoría + { + _logger.LogWarning("Acceso denegado a GetTodoElHistorialDeUsuarios para Usuario ID {UserId}", GetCurrentUserId() ?? 0); + return Forbid(); + } + + var historial = await _usuarioService.ObtenerTodoElHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion); + return Ok(historial); + } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Contables/INotaCreditoDebitoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/INotaCreditoDebitoRepository.cs new file mode 100644 index 0000000..1a3ec4e --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/INotaCreditoDebitoRepository.cs @@ -0,0 +1,21 @@ +using GestionIntegral.Api.Models.Contables; +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Contables +{ + public interface INotaCreditoDebitoRepository + { + Task> GetAllAsync( + DateTime? fechaDesde, DateTime? fechaHasta, + string? destino, int? idDestino, int? idEmpresa, string? tipoNota); + + Task GetByIdAsync(int idNota); + Task CreateAsync(NotaCreditoDebito nuevaNota, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(NotaCreditoDebito notaAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int idNota, int idUsuario, IDbTransaction transaction); + // No se suele validar unicidad por referencia, ya que podría repetirse. + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Contables/IPagoDistribuidorRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/IPagoDistribuidorRepository.cs new file mode 100644 index 0000000..bc5f711 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/IPagoDistribuidorRepository.cs @@ -0,0 +1,21 @@ +using GestionIntegral.Api.Models.Contables; +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Contables +{ + public interface IPagoDistribuidorRepository + { + Task> GetAllAsync( + DateTime? fechaDesde, DateTime? fechaHasta, + int? idDistribuidor, int? idEmpresa, string? tipoMovimiento); + + Task GetByIdAsync(int idPago); + Task CreateAsync(PagoDistribuidor nuevoPago, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(PagoDistribuidor pagoAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int idPago, int idUsuario, IDbTransaction transaction); + Task ExistsByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Contables/NotaCreditoDebitoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/NotaCreditoDebitoRepository.cs new file mode 100644 index 0000000..06d88e9 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/NotaCreditoDebitoRepository.cs @@ -0,0 +1,153 @@ +using Dapper; +using GestionIntegral.Api.Models.Contables; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Contables +{ + public class NotaCreditoDebitoRepository : INotaCreditoDebitoRepository + { + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + + public NotaCreditoDebitoRepository(DbConnectionFactory cf, ILogger log) + { + _cf = cf; + _log = log; + } + + private string SelectQueryBase() => @" + SELECT + Id_Nota AS IdNota, Destino, Id_Destino AS IdDestino, Referencia, Tipo, Fecha, + Monto, Observaciones, Id_Empresa AS IdEmpresa + FROM dbo.cue_CreditosDebitos"; + + public async Task> GetAllAsync( + DateTime? fechaDesde, DateTime? fechaHasta, + string? destino, int? idDestino, int? idEmpresa, string? tipoNota) + { + var sqlBuilder = new StringBuilder(SelectQueryBase()); + sqlBuilder.Append(" WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (fechaDesde.HasValue) { sqlBuilder.Append(" AND Fecha >= @FechaDesdeParam"); parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); } + if (fechaHasta.HasValue) { sqlBuilder.Append(" AND Fecha <= @FechaHastaParam"); parameters.Add("FechaHastaParam", fechaHasta.Value.Date); } + if (!string.IsNullOrWhiteSpace(destino)) { sqlBuilder.Append(" AND Destino = @DestinoParam"); parameters.Add("DestinoParam", destino); } + if (idDestino.HasValue) { sqlBuilder.Append(" AND Id_Destino = @IdDestinoParam"); parameters.Add("IdDestinoParam", idDestino.Value); } + if (idEmpresa.HasValue) { sqlBuilder.Append(" AND Id_Empresa = @IdEmpresaParam"); parameters.Add("IdEmpresaParam", idEmpresa.Value); } + if (!string.IsNullOrWhiteSpace(tipoNota)) { sqlBuilder.Append(" AND Tipo = @TipoParam"); parameters.Add("TipoParam", tipoNota); } + + sqlBuilder.Append(" ORDER BY Fecha DESC, Id_Nota DESC;"); + + try + { + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener todas las Notas de Crédito/Débito."); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int idNota) + { + var sql = SelectQueryBase() + " WHERE Id_Nota = @IdNotaParam"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdNotaParam = idNota }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener Nota C/D por ID: {IdNota}", idNota); + return null; + } + } + + public async Task CreateAsync(NotaCreditoDebito nuevaNota, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.cue_CreditosDebitos (Destino, Id_Destino, Referencia, Tipo, Fecha, Monto, Observaciones, Id_Empresa) + OUTPUT INSERTED.Id_Nota AS IdNota, INSERTED.Destino, INSERTED.Id_Destino AS IdDestino, INSERTED.Referencia, + INSERTED.Tipo, INSERTED.Fecha, INSERTED.Monto, INSERTED.Observaciones, INSERTED.Id_Empresa AS IdEmpresa + VALUES (@Destino, @IdDestino, @Referencia, @Tipo, @Fecha, @Monto, @Observaciones, @IdEmpresa);"; + const string sqlHistorico = @" + INSERT INTO dbo.cue_CreditosDebitos_H + (Id_Nota, Destino, Id_Destino, Referencia, Tipo, Fecha, Monto, Observaciones, Id_Empresa, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdNotaHist, @DestinoHist, @IdDestinoHist, @ReferenciaHist, @TipoHist, @FechaHist, @MontoHist, @ObservacionesHist, @IdEmpresaHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevaNota, transaction); + if (inserted == null || inserted.IdNota == 0) throw new DataException("Error al crear la nota o ID no generado."); + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdNotaHist = inserted.IdNota, DestinoHist = inserted.Destino, IdDestinoHist = inserted.IdDestino, + ReferenciaHist = inserted.Referencia, TipoHist = inserted.Tipo, FechaHist = inserted.Fecha, MontoHist = inserted.Monto, + ObservacionesHist = inserted.Observaciones, IdEmpresaHist = inserted.IdEmpresa, + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Creada" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(NotaCreditoDebito notaAActualizar, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + SelectQueryBase() + " WHERE Id_Nota = @IdNotaParam", + new { IdNotaParam = notaAActualizar.IdNota }, transaction); + if (actual == null) throw new KeyNotFoundException("Nota de Crédito/Débito no encontrada."); + + // Solo se permite actualizar Monto y Observaciones + const string sqlUpdate = @" + UPDATE dbo.cue_CreditosDebitos SET + Monto = @Monto, Observaciones = @Observaciones + WHERE Id_Nota = @IdNota;"; + const string sqlHistorico = @" + INSERT INTO dbo.cue_CreditosDebitos_H + (Id_Nota, Destino, Id_Destino, Referencia, Tipo, Fecha, Monto, Observaciones, Id_Empresa, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdNotaHist, @DestinoHist, @IdDestinoHist, @ReferenciaHist, @TipoHist, @FechaHist, @MontoHist, @ObservacionesHist, @IdEmpresaHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdNotaHist = actual.IdNota, DestinoHist = actual.Destino, IdDestinoHist = actual.IdDestino, ReferenciaHist = actual.Referencia, + TipoHist = actual.Tipo, FechaHist = actual.Fecha, MontoHist = actual.Monto, // Valor ANTERIOR + ObservacionesHist = actual.Observaciones, IdEmpresaHist = actual.IdEmpresa, // Valor ANTERIOR + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Actualizada" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, new { + notaAActualizar.Monto, + notaAActualizar.Observaciones, + notaAActualizar.IdNota + } , transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int idNota, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + SelectQueryBase() + " WHERE Id_Nota = @IdNotaParam", + new { IdNotaParam = idNota }, transaction); + if (actual == null) throw new KeyNotFoundException("Nota de Crédito/Débito no encontrada para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.cue_CreditosDebitos WHERE Id_Nota = @IdNotaParam"; + const string sqlHistorico = @" + INSERT INTO dbo.cue_CreditosDebitos_H + (Id_Nota, Destino, Id_Destino, Referencia, Tipo, Fecha, Monto, Observaciones, Id_Empresa, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdNotaHist, @DestinoHist, @IdDestinoHist, @ReferenciaHist, @TipoHist, @FechaHist, @MontoHist, @ObservacionesHist, @IdEmpresaHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdNotaHist = actual.IdNota, DestinoHist = actual.Destino, IdDestinoHist = actual.IdDestino, ReferenciaHist = actual.Referencia, + TipoHist = actual.Tipo, FechaHist = actual.Fecha, MontoHist = actual.Monto, ObservacionesHist = actual.Observaciones, IdEmpresaHist = actual.IdEmpresa, + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Eliminada" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdNotaParam = idNota }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Contables/PagoDistribuidorRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/PagoDistribuidorRepository.cs new file mode 100644 index 0000000..78d3e21 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/PagoDistribuidorRepository.cs @@ -0,0 +1,175 @@ +using Dapper; +using GestionIntegral.Api.Models.Contables; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Contables +{ + public class PagoDistribuidorRepository : IPagoDistribuidorRepository + { + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + + public PagoDistribuidorRepository(DbConnectionFactory cf, ILogger log) + { + _cf = cf; + _log = log; + } + + private string SelectQueryBase() => @" + SELECT + Id_Pago AS IdPago, Id_Distribuidor AS IdDistribuidor, Fecha, TipoMovimiento, Recibo, Monto, + Id_TipoPago AS IdTipoPago, Detalle, Id_Empresa AS IdEmpresa + FROM dbo.cue_PagosDistribuidor"; + + public async Task> GetAllAsync( + DateTime? fechaDesde, DateTime? fechaHasta, + int? idDistribuidor, int? idEmpresa, string? tipoMovimiento) + { + var sqlBuilder = new StringBuilder(SelectQueryBase()); + sqlBuilder.Append(" WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (fechaDesde.HasValue) { sqlBuilder.Append(" AND Fecha >= @FechaDesdeParam"); parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); } + if (fechaHasta.HasValue) { sqlBuilder.Append(" AND Fecha <= @FechaHastaParam"); parameters.Add("FechaHastaParam", fechaHasta.Value.Date); } + if (idDistribuidor.HasValue) { sqlBuilder.Append(" AND Id_Distribuidor = @IdDistribuidorParam"); parameters.Add("IdDistribuidorParam", idDistribuidor.Value); } + if (idEmpresa.HasValue) { sqlBuilder.Append(" AND Id_Empresa = @IdEmpresaParam"); parameters.Add("IdEmpresaParam", idEmpresa.Value); } + if (!string.IsNullOrWhiteSpace(tipoMovimiento)) { sqlBuilder.Append(" AND TipoMovimiento = @TipoMovParam"); parameters.Add("TipoMovParam", tipoMovimiento); } + + sqlBuilder.Append(" ORDER BY Fecha DESC, Id_Pago DESC;"); + + try + { + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener todos los Pagos de Distribuidores."); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int idPago) + { + var sql = SelectQueryBase() + " WHERE Id_Pago = @IdPagoParam"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdPagoParam = idPago }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener PagoDistribuidor por ID: {IdPago}", idPago); + return null; + } + } + + public async Task ExistsByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.cue_PagosDistribuidor WHERE Recibo = @ReciboParam AND TipoMovimiento = @TipoMovParam"); + var parameters = new DynamicParameters(); + parameters.Add("ReciboParam", recibo); + parameters.Add("TipoMovParam", tipoMovimiento); + + if (excludeIdPago.HasValue) + { + sqlBuilder.Append(" AND Id_Pago != @ExcludeIdPagoParam"); + parameters.Add("ExcludeIdPagoParam", excludeIdPago.Value); + } + try + { + using var connection = _cf.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error en ExistsByReciboAndTipoMovimientoAsync. Recibo: {Recibo}, Tipo: {Tipo}", recibo, tipoMovimiento); + return true; // Asumir que existe en caso de error para prevenir duplicados + } + } + + public async Task CreateAsync(PagoDistribuidor nuevoPago, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.cue_PagosDistribuidor (Id_Distribuidor, Fecha, TipoMovimiento, Recibo, Monto, Id_TipoPago, Detalle, Id_Empresa) + OUTPUT INSERTED.Id_Pago AS IdPago, INSERTED.Id_Distribuidor AS IdDistribuidor, INSERTED.Fecha, INSERTED.TipoMovimiento, + INSERTED.Recibo, INSERTED.Monto, INSERTED.Id_TipoPago AS IdTipoPago, INSERTED.Detalle, INSERTED.Id_Empresa AS IdEmpresa + VALUES (@IdDistribuidor, @Fecha, @TipoMovimiento, @Recibo, @Monto, @IdTipoPago, @Detalle, @IdEmpresa);"; + const string sqlHistorico = @" + INSERT INTO dbo.cue_PagosDistribuidor_H + (Id_Pago, Id_Distribuidor, Fecha, TipoMovimiento, Recibo, Monto, Id_TipoPago, Detalle, Id_Empresa, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPagoHist, @IdDistribuidorHist, @FechaHist, @TipoMovimientoHist, @ReciboHist, @MontoHist, @IdTipoPagoHist, @DetalleHist, @IdEmpresaHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevoPago, transaction); + if (inserted == null || inserted.IdPago == 0) throw new DataException("Error al crear pago o ID no generado."); + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdPagoHist = inserted.IdPago, IdDistribuidorHist = inserted.IdDistribuidor, FechaHist = inserted.Fecha, + TipoMovimientoHist = inserted.TipoMovimiento, ReciboHist = inserted.Recibo, MontoHist = inserted.Monto, + IdTipoPagoHist = inserted.IdTipoPago, DetalleHist = inserted.Detalle, IdEmpresaHist = inserted.IdEmpresa, + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Creado" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(PagoDistribuidor pagoAActualizar, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + SelectQueryBase() + " WHERE Id_Pago = @IdPagoParam", + new { IdPagoParam = pagoAActualizar.IdPago }, transaction); + if (actual == null) throw new KeyNotFoundException("Pago no encontrado."); + + // Campos que se permiten actualizar: Monto, Id_TipoPago, Detalle + // Otros campos como IdDistribuidor, Fecha, TipoMovimiento, Recibo, IdEmpresa no deberían cambiar + // para un pago ya registrado. Si necesitan cambiar, se debería anular/eliminar y crear uno nuevo. + const string sqlUpdate = @" + UPDATE dbo.cue_PagosDistribuidor SET + Monto = @Monto, Id_TipoPago = @IdTipoPago, Detalle = @Detalle + WHERE Id_Pago = @IdPago;"; + const string sqlHistorico = @" + INSERT INTO dbo.cue_PagosDistribuidor_H + (Id_Pago, Id_Distribuidor, Fecha, TipoMovimiento, Recibo, Monto, Id_TipoPago, Detalle, Id_Empresa, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPagoHist, @IdDistribuidorHist, @FechaHist, @TipoMovimientoHist, @ReciboHist, @MontoHist, @IdTipoPagoHist, @DetalleHist, @IdEmpresaHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdPagoHist = actual.IdPago, IdDistribuidorHist = actual.IdDistribuidor, FechaHist = actual.Fecha, + TipoMovimientoHist = actual.TipoMovimiento, ReciboHist = actual.Recibo, MontoHist = actual.Monto, // Valor ANTERIOR + IdTipoPagoHist = actual.IdTipoPago, DetalleHist = actual.Detalle, IdEmpresaHist = actual.IdEmpresa, // Valores ANTERIORES + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Actualizado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, pagoAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int idPago, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + SelectQueryBase() + " WHERE Id_Pago = @IdPagoParam", + new { IdPagoParam = idPago }, transaction); + if (actual == null) throw new KeyNotFoundException("Pago no encontrado para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.cue_PagosDistribuidor WHERE Id_Pago = @IdPagoParam"; + const string sqlHistorico = @" + INSERT INTO dbo.cue_PagosDistribuidor_H + (Id_Pago, Id_Distribuidor, Fecha, TipoMovimiento, Recibo, Monto, Id_TipoPago, Detalle, Id_Empresa, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPagoHist, @IdDistribuidorHist, @FechaHist, @TipoMovimientoHist, @ReciboHist, @MontoHist, @IdTipoPagoHist, @DetalleHist, @IdEmpresaHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdPagoHist = actual.IdPago, IdDistribuidorHist = actual.IdDistribuidor, FechaHist = actual.Fecha, + TipoMovimientoHist = actual.TipoMovimiento, ReciboHist = actual.Recibo, MontoHist = actual.Monto, + IdTipoPagoHist = actual.IdTipoPago, DetalleHist = actual.Detalle, IdEmpresaHist = actual.IdEmpresa, + IdUsuarioHist = idUsuario, FechaModHist = DateTime.Now, TipoModHist = "Eliminado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdPagoParam = idPago }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ControlDevolucionesRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ControlDevolucionesRepository.cs new file mode 100644 index 0000000..66258c1 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ControlDevolucionesRepository.cs @@ -0,0 +1,162 @@ +using Dapper; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class ControlDevolucionesRepository : IControlDevolucionesRepository + { + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + + public ControlDevolucionesRepository(DbConnectionFactory cf, ILogger log) + { + _cf = cf; + _log = log; + } + + private string SelectQueryBase() => @" + SELECT + Id_Control AS IdControl, Id_Empresa AS IdEmpresa, Fecha, + Entrada, Sobrantes, Detalle, SinCargo + FROM dbo.dist_dtCtrlDevoluciones"; + + public async Task> GetAllAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idEmpresa) + { + var sqlBuilder = new StringBuilder(SelectQueryBase()); + sqlBuilder.Append(" WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (fechaDesde.HasValue) { sqlBuilder.Append(" AND Fecha >= @FechaDesdeParam"); parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); } + if (fechaHasta.HasValue) { sqlBuilder.Append(" AND Fecha <= @FechaHastaParam"); parameters.Add("FechaHastaParam", fechaHasta.Value.Date); } + if (idEmpresa.HasValue) { sqlBuilder.Append(" AND Id_Empresa = @IdEmpresaParam"); parameters.Add("IdEmpresaParam", idEmpresa.Value); } + + sqlBuilder.Append(" ORDER BY Fecha DESC, Id_Empresa;"); + + try + { + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener todos los Controles de Devoluciones."); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int idControl) + { + var sql = SelectQueryBase() + " WHERE Id_Control = @IdControlParam"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdControlParam = idControl }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener ControlDevoluciones por ID: {IdControl}", idControl); + return null; + } + } + + public async Task GetByEmpresaAndFechaAsync(int idEmpresa, DateTime fecha, IDbTransaction? transaction = null) + { + var sql = SelectQueryBase() + " WHERE Id_Empresa = @IdEmpresaParam AND Fecha = @FechaParam"; + var cn = transaction?.Connection ?? _cf.CreateConnection(); + bool ownConnection = transaction == null; + ControlDevoluciones? result = null; + try + { + if (ownConnection && cn.State == ConnectionState.Closed && cn is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); + result = await cn.QuerySingleOrDefaultAsync(sql, new { IdEmpresaParam = idEmpresa, FechaParam = fecha.Date }, transaction); + } + finally + { + if (ownConnection && cn.State == ConnectionState.Open && cn is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); + if (ownConnection) (cn as IDisposable)?.Dispose(); + } + return result; + } + + + public async Task CreateAsync(ControlDevoluciones nuevoControl, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_dtCtrlDevoluciones (Id_Empresa, Fecha, Entrada, Sobrantes, Detalle, SinCargo) + OUTPUT INSERTED.Id_Control AS IdControl, INSERTED.Id_Empresa AS IdEmpresa, INSERTED.Fecha, + INSERTED.Entrada, INSERTED.Sobrantes, INSERTED.Detalle, INSERTED.SinCargo + VALUES (@IdEmpresa, @Fecha, @Entrada, @Sobrantes, @Detalle, @SinCargo);"; + const string sqlHistorico = @" + INSERT INTO dbo.dist_dtCtrlDevoluciones_H + (Id_Control, Id_Empresa, Fecha, Entrada, Sobrantes, Detalle, SinCargo, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdControlParam, @IdEmpresaParam, @FechaParam, @EntradaParam, @SobrantesParam, @DetalleParam, @SinCargoParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevoControl, transaction); + if (inserted == null || inserted.IdControl == 0) throw new DataException("Error al crear control de devoluciones o ID no generado."); + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdControlParam = inserted.IdControl, IdEmpresaParam = inserted.IdEmpresa, FechaParam = inserted.Fecha, + EntradaParam = inserted.Entrada, SobrantesParam = inserted.Sobrantes, DetalleParam = inserted.Detalle, SinCargoParam = inserted.SinCargo, + IdUsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Creado" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(ControlDevoluciones controlAActualizar, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + SelectQueryBase() + " WHERE Id_Control = @IdControlParam", + new { IdControlParam = controlAActualizar.IdControl }, transaction); + if (actual == null) throw new KeyNotFoundException("Control de devoluciones no encontrado."); + + // En este caso, Id_Empresa y Fecha no deberían cambiar para un registro existente + const string sqlUpdate = @" + UPDATE dbo.dist_dtCtrlDevoluciones SET + Entrada = @Entrada, Sobrantes = @Sobrantes, Detalle = @Detalle, SinCargo = @SinCargo + WHERE Id_Control = @IdControl;"; + const string sqlHistorico = @" + INSERT INTO dbo.dist_dtCtrlDevoluciones_H + (Id_Control, Id_Empresa, Fecha, Entrada, Sobrantes, Detalle, SinCargo, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdControlParam, @IdEmpresaParam, @FechaParam, @EntradaParam, @SobrantesParam, @DetalleParam, @SinCargoParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdControlParam = actual.IdControl, IdEmpresaParam = actual.IdEmpresa, FechaParam = actual.Fecha, // Datos originales + EntradaParam = actual.Entrada, SobrantesParam = actual.Sobrantes, DetalleParam = actual.Detalle, SinCargoParam = actual.SinCargo, // Valores ANTERIORES + IdUsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Actualizado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, controlAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task DeleteAsync(int idControl, int idUsuario, IDbTransaction transaction) + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync( + SelectQueryBase() + " WHERE Id_Control = @IdControlParam", + new { IdControlParam = idControl }, transaction); + if (actual == null) throw new KeyNotFoundException("Control de devoluciones no encontrado para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.dist_dtCtrlDevoluciones WHERE Id_Control = @IdControlParam"; + const string sqlHistorico = @" + INSERT INTO dbo.dist_dtCtrlDevoluciones_H + (Id_Control, Id_Empresa, Fecha, Entrada, Sobrantes, Detalle, SinCargo, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdControlParam, @IdEmpresaParam, @FechaParam, @EntradaParam, @SobrantesParam, @DetalleParam, @SinCargoParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new { + IdControlParam = actual.IdControl, IdEmpresaParam = actual.IdEmpresa, FechaParam = actual.Fecha, + EntradaParam = actual.Entrada, SobrantesParam = actual.Sobrantes, DetalleParam = actual.Detalle, SinCargoParam = actual.SinCargo, + IdUsuarioParam = idUsuario, FechaModParam = DateTime.Now, TipoModParam = "Eliminado" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdControlParam = idControl }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EntradaSalidaCanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EntradaSalidaCanillaRepository.cs new file mode 100644 index 0000000..e761aef --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EntradaSalidaCanillaRepository.cs @@ -0,0 +1,298 @@ +using Dapper; +using GestionIntegral.Api.Models.Distribucion; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class EntradaSalidaCanillaRepository : IEntradaSalidaCanillaRepository + { + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + + public EntradaSalidaCanillaRepository(DbConnectionFactory cf, ILogger log) + { + _cf = cf; + _log = log; + } + + private string SelectQueryBase() => @" + SELECT + Id_Parte AS IdParte, Id_Publicacion AS IdPublicacion, Id_Canilla AS IdCanilla, + Fecha, CantSalida, CantEntrada, Id_Precio AS IdPrecio, Id_Recargo AS IdRecargo, + Id_PorcMon AS IdPorcMon, Observacion, Liquidado, FechaLiquidado, UserLiq + FROM dbo.dist_EntradasSalidasCanillas"; + + public async Task> GetAllAsync( + DateTime? fechaDesde, DateTime? fechaHasta, + int? idPublicacion, int? idCanilla, bool? liquidados) + { + var sqlBuilder = new StringBuilder(SelectQueryBase()); + sqlBuilder.Append(" WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (fechaDesde.HasValue) { sqlBuilder.Append(" AND Fecha >= @FechaDesdeParam"); parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); } + if (fechaHasta.HasValue) { sqlBuilder.Append(" AND Fecha <= @FechaHastaParam"); parameters.Add("FechaHastaParam", fechaHasta.Value.Date); } + if (idPublicacion.HasValue) { sqlBuilder.Append(" AND Id_Publicacion = @IdPublicacionParam"); parameters.Add("IdPublicacionParam", idPublicacion.Value); } + if (idCanilla.HasValue) { sqlBuilder.Append(" AND Id_Canilla = @IdCanillaParam"); parameters.Add("IdCanillaParam", idCanilla.Value); } + if (liquidados.HasValue) { sqlBuilder.Append(" AND Liquidado = @LiquidadoParam"); parameters.Add("LiquidadoParam", liquidados.Value); } + + sqlBuilder.Append(" ORDER BY Fecha DESC, Id_Canilla, Id_Publicacion, Id_Parte DESC;"); + + try + { + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener Entradas/Salidas de Canillitas."); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int idParte) + { + var sql = SelectQueryBase() + " WHERE Id_Parte = @IdParteParam"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdParteParam = idParte }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener EntradaSalidaCanilla por ID: {IdParte}", idParte); + return null; + } + } + + public async Task> GetByIdsAsync(IEnumerable idsPartes, IDbTransaction? transaction = null) + { + if (idsPartes == null || !idsPartes.Any()) return Enumerable.Empty(); + var sql = SelectQueryBase() + " WHERE Id_Parte IN @IdsPartesParam"; + + var cn = transaction?.Connection ?? _cf.CreateConnection(); + bool ownConnection = transaction == null; + IEnumerable result = Enumerable.Empty(); + try + { + if (ownConnection && cn.State == ConnectionState.Closed && cn is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); + result = await cn.QueryAsync(sql, new { IdsPartesParam = idsPartes }, transaction); + } + finally + { + if (ownConnection && cn.State == ConnectionState.Open && cn is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); + if (ownConnection) (cn as IDisposable)?.Dispose(); + } + return result; + } + + public async Task ExistsByPublicacionCanillaFechaAsync(int idPublicacion, int idCanilla, DateTime fecha, IDbTransaction? transaction = null, int? excludeIdParte = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_EntradasSalidasCanillas WHERE Id_Publicacion = @IdPubParam AND Id_Canilla = @IdCanParam AND Fecha = @FechaParam"); + var parameters = new DynamicParameters(); + parameters.Add("IdPubParam", idPublicacion); + parameters.Add("IdCanParam", idCanilla); + parameters.Add("FechaParam", fecha.Date); + + if (excludeIdParte.HasValue) + { + sqlBuilder.Append(" AND Id_Parte != @ExcludeIdParteParam"); + parameters.Add("ExcludeIdParteParam", excludeIdParte.Value); + } + + var connection = transaction?.Connection ?? _cf.CreateConnection(); + bool ownConnection = transaction == null; + bool result = false; + try + { + if (ownConnection && connection.State == ConnectionState.Closed && connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); + + result = await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters, transaction); + + } + catch (Exception ex) + { + _log.LogError(ex, "Error en ExistsByPublicacionCanillaFechaAsync."); + return true; // Asumir que existe en caso de error + } + finally + { + if (ownConnection && connection.State == ConnectionState.Open && connection is System.Data.Common.DbConnection dbConnClose) await dbConnClose.CloseAsync(); + if (ownConnection) (connection as IDisposable)?.Dispose(); + } + return result; + } + + + public async Task CreateAsync(EntradaSalidaCanilla nuevoES, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.dist_EntradasSalidasCanillas + (Id_Publicacion, Id_Canilla, Fecha, CantSalida, CantEntrada, Id_Precio, Id_Recargo, Id_PorcMon, Observacion, Liquidado, FechaLiquidado, UserLiq) + OUTPUT INSERTED.Id_Parte AS IdParte, INSERTED.Id_Publicacion AS IdPublicacion, INSERTED.Id_Canilla AS IdCanilla, INSERTED.Fecha, + INSERTED.CantSalida, INSERTED.CantEntrada, INSERTED.Id_Precio AS IdPrecio, INSERTED.Id_Recargo AS IdRecargo, + INSERTED.Id_PorcMon AS IdPorcMon, INSERTED.Observacion, INSERTED.Liquidado, INSERTED.FechaLiquidado, INSERTED.UserLiq + VALUES (@IdPublicacion, @IdCanilla, @Fecha, @CantSalida, @CantEntrada, @IdPrecio, @IdRecargo, @IdPorcMon, @Observacion, @Liquidado, @FechaLiquidado, @UserLiq);"; + + var inserted = await transaction.Connection!.QuerySingleAsync(sqlInsert, nuevoES, transaction); + if (inserted == null || inserted.IdParte == 0) throw new DataException("Error al crear E/S Canilla o ID no generado."); + + const string sqlHistorico = @" + INSERT INTO dbo.dist_EntradasSalidasCanillas_H + (Id_Parte, Id_Publicacion, Id_Canilla, Fecha, CantSalida, CantEntrada, Id_Precio, Id_Recargo, Id_PorcMon, Observacion, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdParteHist, @IdPubHist, @IdCanillaHist, @FechaHist, @CantSalidaHist, @CantEntradaHist, @IdPrecioHist, @IdRecargoHist, @IdPorcMonHist, @ObsHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + // Liquidado, FechaLiquidado, UserLiq no van al historial de creación, sino al de liquidación. + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new + { + IdParteHist = inserted.IdParte, + IdPubHist = inserted.IdPublicacion, + IdCanillaHist = inserted.IdCanilla, + FechaHist = inserted.Fecha, + CantSalidaHist = inserted.CantSalida, + CantEntradaHist = inserted.CantEntrada, + IdPrecioHist = inserted.IdPrecio, + IdRecargoHist = inserted.IdRecargo, + IdPorcMonHist = inserted.IdPorcMon, + ObsHist = inserted.Observacion, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Creada" + }, transaction); + return inserted; + } + + public async Task UpdateAsync(EntradaSalidaCanilla esAActualizar, int idUsuario, IDbTransaction transaction, string tipoMod = "Actualizada") + { + var actual = await transaction.Connection!.QuerySingleOrDefaultAsync(SelectQueryBase() + " WHERE Id_Parte = @IdParteParam", + new { IdParteParam = esAActualizar.IdParte }, transaction); + if (actual == null) throw new KeyNotFoundException("Registro E/S Canilla no encontrado."); + + const string sqlUpdate = @" + UPDATE dbo.dist_EntradasSalidasCanillas SET + CantSalida = @CantSalida, CantEntrada = @CantEntrada, Observacion = @Observacion, + Liquidado = @Liquidado, FechaLiquidado = @FechaLiquidado, UserLiq = @UserLiq, + Id_Precio = @IdPrecio, Id_Recargo = @IdRecargo, Id_PorcMon = @IdPorcMon + -- No se permite cambiar Publicacion, Canilla, Fecha directamente aquí. + WHERE Id_Parte = @IdParte;"; + + const string sqlHistorico = @" + INSERT INTO dbo.dist_EntradasSalidasCanillas_H + (Id_Parte, Id_Publicacion, Id_Canilla, Fecha, CantSalida, CantEntrada, Id_Precio, Id_Recargo, Id_PorcMon, Observacion, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdParteHist, @IdPubHist, @IdCanillaHist, @FechaHist, @CantSalidaHist, @CantEntradaHist, @IdPrecioHist, @IdRecargoHist, @IdPorcMonHist, @ObsHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new + { + IdParteHist = actual.IdParte, + IdPubHist = actual.IdPublicacion, + IdCanillaHist = actual.IdCanilla, + FechaHist = actual.Fecha, + CantSalidaHist = actual.CantSalida, + CantEntradaHist = actual.CantEntrada, + IdPrecioHist = actual.IdPrecio, + IdRecargoHist = actual.IdRecargo, + IdPorcMonHist = actual.IdPorcMon, + ObsHist = actual.Observacion, // Valores ANTERIORES + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = tipoMod + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, esAActualizar, transaction); + return rowsAffected == 1; + } + + public async Task LiquidarAsync(IEnumerable idsPartes, DateTime fechaLiquidacion, int idUsuarioLiquidador, IDbTransaction transaction) + { + // Primero, obtener los registros actuales para el historial + var movimientosALiquidar = await GetByIdsAsync(idsPartes, transaction); + if (!movimientosALiquidar.Any() || movimientosALiquidar.Count() != idsPartes.Distinct().Count()) + { + _log.LogWarning("Intento de liquidar IdsPartes no encontrados o inconsistentes."); + return false; // O lanzar excepción + } + + const string sqlUpdate = @" + UPDATE dbo.dist_EntradasSalidasCanillas SET + Liquidado = 1, FechaLiquidado = @FechaLiquidacionParam, UserLiq = @UserLiqParam + WHERE Id_Parte = @IdParteParam AND Liquidado = 0;"; // Solo liquidar los no liquidados + + const string sqlHistorico = @" + INSERT INTO dbo.dist_EntradasSalidasCanillas_H + (Id_Parte, Id_Publicacion, Id_Canilla, Fecha, CantSalida, CantEntrada, Id_Precio, Id_Recargo, Id_PorcMon, Observacion, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdParteHist, @IdPubHist, @IdCanillaHist, @FechaHist, @CantSalidaHist, @CantEntradaHist, @IdPrecioHist, @IdRecargoHist, @IdPorcMonHist, @ObsHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + int totalRowsAffected = 0; + foreach (var mov in movimientosALiquidar) + { + if (mov.Liquidado) continue; // Ya estaba liquidado, no hacer nada ni registrar historial + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new + { + IdParteHist = mov.IdParte, + IdPubHist = mov.IdPublicacion, + IdCanillaHist = mov.IdCanilla, + FechaHist = mov.Fecha, + CantSalidaHist = mov.CantSalida, + CantEntradaHist = mov.CantEntrada, + IdPrecioHist = mov.IdPrecio, + IdRecargoHist = mov.IdRecargo, + IdPorcMonHist = mov.IdPorcMon, + ObsHist = mov.Observacion, + IdUsuarioHist = idUsuarioLiquidador, + FechaModHist = DateTime.Now, + TipoModHist = "Liquidada" + }, transaction); + + var rows = await transaction.Connection!.ExecuteAsync(sqlUpdate, + new { FechaLiquidacionParam = fechaLiquidacion.Date, UserLiqParam = idUsuarioLiquidador, IdParteParam = mov.IdParte }, + transaction); + totalRowsAffected += rows; + } + // Se considera éxito si al menos una fila fue afectada (o si todas ya estaban liquidadas y no hubo errores) + // Si se pasaron IDs que no existen, GetByIdsAsync ya devolvería menos elementos. + return totalRowsAffected >= 0; + } + + + public async Task DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction) + { + var actual = await GetByIdAsync(idParte); // No necesita TX, solo para el historial + if (actual == null) throw new KeyNotFoundException("Registro E/S Canilla no encontrado para eliminar."); + if (actual.Liquidado) throw new InvalidOperationException("No se puede eliminar un movimiento liquidado."); + + + const string sqlDelete = "DELETE FROM dbo.dist_EntradasSalidasCanillas WHERE Id_Parte = @IdParteParam"; + const string sqlHistorico = @" + INSERT INTO dbo.dist_EntradasSalidasCanillas_H + (Id_Parte, Id_Publicacion, Id_Canilla, Fecha, CantSalida, CantEntrada, Id_Precio, Id_Recargo, Id_PorcMon, Observacion, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdParteHist, @IdPubHist, @IdCanillaHist, @FechaHist, @CantSalidaHist, @CantEntradaHist, @IdPrecioHist, @IdRecargoHist, @IdPorcMonHist, @ObsHist, @IdUsuarioHist, @FechaModHist, @TipoModHist);"; + + await transaction.Connection!.ExecuteAsync(sqlHistorico, new + { + IdParteHist = actual.IdParte, + IdPubHist = actual.IdPublicacion, + IdCanillaHist = actual.IdCanilla, + FechaHist = actual.Fecha, + CantSalidaHist = actual.CantSalida, + CantEntradaHist = actual.CantEntrada, + IdPrecioHist = actual.IdPrecio, + IdRecargoHist = actual.IdRecargo, + IdPorcMonHist = actual.IdPorcMon, + ObsHist = actual.Observacion, + IdUsuarioHist = idUsuario, + FechaModHist = DateTime.Now, + TipoModHist = "Eliminada" + }, transaction); + + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { IdParteParam = idParte }, transaction); + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IControlDevolucionesRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IControlDevolucionesRepository.cs new file mode 100644 index 0000000..a659196 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IControlDevolucionesRepository.cs @@ -0,0 +1,18 @@ +using GestionIntegral.Api.Models.Distribucion; +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IControlDevolucionesRepository + { + Task> GetAllAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idEmpresa); + Task GetByIdAsync(int idControl); + Task GetByEmpresaAndFechaAsync(int idEmpresa, DateTime fecha, IDbTransaction? transaction = null); // Para validar unicidad + Task CreateAsync(ControlDevoluciones nuevoControl, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(ControlDevoluciones controlAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int idControl, int idUsuario, IDbTransaction transaction); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEntradaSalidaCanillaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEntradaSalidaCanillaRepository.cs new file mode 100644 index 0000000..5c04ce1 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEntradaSalidaCanillaRepository.cs @@ -0,0 +1,23 @@ +using GestionIntegral.Api.Models.Distribucion; +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IEntradaSalidaCanillaRepository + { + Task> GetAllAsync( + DateTime? fechaDesde, DateTime? fechaHasta, + int? idPublicacion, int? idCanilla, bool? liquidados); + + Task GetByIdAsync(int idParte); + Task> GetByIdsAsync(IEnumerable idsPartes, IDbTransaction? transaction = null); // Para liquidación masiva + Task CreateAsync(EntradaSalidaCanilla nuevoES, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(EntradaSalidaCanilla esAActualizar, int idUsuario, IDbTransaction transaction, string tipoMod = "Actualizada"); + Task DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction); + Task LiquidarAsync(IEnumerable idsPartes, DateTime fechaLiquidacion, int idUsuarioLiquidador, IDbTransaction transaction); + Task ExistsByPublicacionCanillaFechaAsync(int idPublicacion, int idCanilla, DateTime fecha, IDbTransaction? transaction = null, int? excludeIdParte = null); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Radios/CancionRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Radios/CancionRepository.cs new file mode 100644 index 0000000..c3cc446 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Radios/CancionRepository.cs @@ -0,0 +1,196 @@ +using Dapper; +using GestionIntegral.Api.Models.Radios; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Radios +{ + public class CancionRepository : ICancionRepository + { + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + + public CancionRepository(DbConnectionFactory cf, ILogger log) + { + _cf = cf; + _log = log; + } + + private string SelectWithAlias() => @" + SELECT + Id, Tema, CompositorAutor, Interprete, Sello, Placa, Pista, Introduccion, + Ritmo AS IdRitmo, -- Mapear columna Ritmo a propiedad IdRitmo del modelo Cancion + Formato, Album + FROM dbo.rad_dtCanciones"; + + + public async Task> GetAllAsync(string? temaFilter, string? interpreteFilter, int? idRitmoFilter) + { + var sqlBuilder = new StringBuilder(SelectWithAlias()); + sqlBuilder.Append(" WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(temaFilter)) + { + sqlBuilder.Append(" AND Tema LIKE @TemaParam"); + parameters.Add("TemaParam", $"%{temaFilter}%"); + } + if (!string.IsNullOrWhiteSpace(interpreteFilter)) + { + sqlBuilder.Append(" AND Interprete LIKE @InterpreteParam"); + parameters.Add("InterpreteParam", $"%{interpreteFilter}%"); + } + if (idRitmoFilter.HasValue) + { + sqlBuilder.Append(" AND Ritmo = @IdRitmoParam"); // La columna en DB es "Ritmo" + parameters.Add("IdRitmoParam", idRitmoFilter.Value); + } + sqlBuilder.Append(" ORDER BY Tema, Interprete;"); + + try + { + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener todas las Canciones."); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int id) + { + var sql = SelectWithAlias() + " WHERE Id = @IdParam"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdParam = id }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener Canción por ID: {IdCancion}", id); + return null; + } + } + + public async Task ExistsByTemaAndInterpreteAsync(string tema, string interprete, int? excludeId = null) + { + // La unicidad puede ser más compleja si se consideran otros campos como Álbum. + // Por ahora, un ejemplo simple. + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.rad_dtCanciones WHERE Tema = @TemaParam AND Interprete = @InterpreteParam"); + var parameters = new DynamicParameters(); + parameters.Add("TemaParam", tema); + parameters.Add("InterpreteParam", interprete); + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND Id != @ExcludeIdParam"); + parameters.Add("ExcludeIdParam", excludeId.Value); + } + try + { + using var connection = _cf.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error en ExistsByTemaAndInterpreteAsync. Tema: {Tema}, Interprete: {Interprete}", tema, interprete); + return true; // Asumir que existe en caso de error + } + } + + + public async Task CreateAsync(Cancion nuevaCancion /*, int idUsuario, IDbTransaction transaction */) + { + // El modelo Cancion usa IdRitmo, pero la tabla rad_dtCanciones usa la columna "Ritmo" para el FK a rad_dtRitmos.Id + const string sqlInsert = @" + INSERT INTO dbo.rad_dtCanciones (Tema, CompositorAutor, Interprete, Sello, Placa, Pista, Introduccion, Ritmo, Formato, Album) + OUTPUT INSERTED.Id, INSERTED.Tema, INSERTED.CompositorAutor, INSERTED.Interprete, INSERTED.Sello, INSERTED.Placa, + INSERTED.Pista, INSERTED.Introduccion, INSERTED.Ritmo AS IdRitmo, INSERTED.Formato, INSERTED.Album + VALUES (@Tema, @CompositorAutor, @Interprete, @Sello, @Placa, @Pista, @Introduccion, @IdRitmo, @Formato, @Album);"; + try + { + using var connection = _cf.CreateConnection(); + // El objeto nuevaCancion tiene la propiedad IdRitmo, que Dapper mapeará al parámetro @IdRitmo + return await connection.QuerySingleAsync(sqlInsert, nuevaCancion); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al crear Canción: {Tema} por {Interprete}", nuevaCancion.Tema, nuevaCancion.Interprete); + return null; + } + } + + public async Task UpdateAsync(Cancion cancionAActualizar /*, int idUsuario, IDbTransaction transaction */) + { + // Similar a Create, la columna en DB es "Ritmo" + const string sqlUpdate = @" + UPDATE dbo.rad_dtCanciones SET + Tema = @Tema, CompositorAutor = @CompositorAutor, Interprete = @Interprete, Sello = @Sello, + Placa = @Placa, Pista = @Pista, Introduccion = @Introduccion, Ritmo = @IdRitmo, + Formato = @Formato, Album = @Album + WHERE Id = @Id;"; + try + { + using var connection = _cf.CreateConnection(); + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, cancionAActualizar); + return rowsAffected == 1; + } + catch (Exception ex) + { + _log.LogError(ex, "Error al actualizar Canción ID: {IdCancion}", cancionAActualizar.Id); + return false; + } + } + + public async Task DeleteAsync(int id /*, int idUsuario, IDbTransaction transaction */) + { + // IsInUseAsync no se implementó porque no hay dependencias directas obvias que impidan borrar una canción + // (a menos que las listas generadas se guarden y referencien IDs de canciones). + const string sqlDelete = "DELETE FROM dbo.rad_dtCanciones WHERE Id = @IdParam"; + try + { + using var connection = _cf.CreateConnection(); + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { IdParam = id }); + return rowsAffected == 1; + } + catch (Exception ex) + { + _log.LogError(ex, "Error al eliminar Canción ID: {IdCancion}", id); + return false; + } + } + // IsInUseAsync podría ser relevante si las listas generadas referencian el Id de la canción + // Por ahora, lo omitimos. + public Task IsInUseAsync(int id) + { + _log.LogWarning("IsInUseAsync no implementado para CancionRepository."); + return Task.FromResult(false); // Asumir que no está en uso por ahora + } + + public async Task> GetRandomCancionesAsync(int count) + { + // El modelo Cancion tiene la propiedad "Ritmo" que es el IdRitmo. + // El SelectWithAlias no es necesario aquí si solo tomamos las columnas que ya tiene Cancion. + // Si el modelo Cancion no tuviera la propiedad Ritmo (IdRitmo), entonces necesitarías el alias. + // Por simplicidad, y dado que el modelo Cancion sí tiene Ritmo (que es IdRitmo), no necesitamos un alias específico aquí. + var sql = $"SELECT TOP ({count}) Id, Tema, CompositorAutor, Interprete, Sello, Placa, Pista, Introduccion, Ritmo, Formato, Album FROM dbo.rad_dtCanciones ORDER BY NEWID()"; + try + { + using var connection = _cf.CreateConnection(); + // Dapper mapeará la columna "Ritmo" de la BD a la propiedad "Ritmo" del modelo Cancion. + return await connection.QueryAsync(sql); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener {Count} canciones aleatorias.", count); + return Enumerable.Empty(); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Radios/ICancionRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Radios/ICancionRepository.cs new file mode 100644 index 0000000..bec78ca --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Radios/ICancionRepository.cs @@ -0,0 +1,18 @@ +using GestionIntegral.Api.Models.Radios; +using System.Collections.Generic; +using System.Threading.Tasks; +// using System.Data; // Solo si se usa transacción para historial + +namespace GestionIntegral.Api.Data.Repositories.Radios +{ + public interface ICancionRepository + { + Task> GetAllAsync(string? temaFilter, string? interpreteFilter, int? idRitmoFilter); + Task GetByIdAsync(int id); + Task CreateAsync(Cancion nuevaCancion /*, int idUsuario, IDbTransaction transaction - si hay historial */); + Task UpdateAsync(Cancion cancionAActualizar /*, int idUsuario, IDbTransaction transaction */); + Task DeleteAsync(int id /*, int idUsuario, IDbTransaction transaction */); + Task ExistsByTemaAndInterpreteAsync(string tema, string interprete, int? excludeId = null); // Ejemplo de unicidad + Task> GetRandomCancionesAsync(int count); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Radios/IRitmoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Radios/IRitmoRepository.cs new file mode 100644 index 0000000..6b17366 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Radios/IRitmoRepository.cs @@ -0,0 +1,18 @@ +using GestionIntegral.Api.Models.Radios; +using System.Collections.Generic; +using System.Threading.Tasks; +// using System.Data; // Solo si se usa transacción para historial + +namespace GestionIntegral.Api.Data.Repositories.Radios +{ + public interface IRitmoRepository + { + Task> GetAllAsync(string? nombreFilter); + Task GetByIdAsync(int id); + Task CreateAsync(Ritmo nuevoRitmo /*, int idUsuario, IDbTransaction transaction - si hay historial */); + Task UpdateAsync(Ritmo ritmoAActualizar /*, int idUsuario, IDbTransaction transaction */); + Task DeleteAsync(int id /*, int idUsuario, IDbTransaction transaction */); + Task ExistsByNameAsync(string nombreRitmo, int? excludeId = null); + Task IsInUseAsync(int id); // Verificar si se usa en rad_dtCanciones + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Radios/RitmoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Radios/RitmoRepository.cs new file mode 100644 index 0000000..77cb7e0 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Radios/RitmoRepository.cs @@ -0,0 +1,149 @@ +using Dapper; +using GestionIntegral.Api.Models.Radios; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Radios +{ + public class RitmoRepository : IRitmoRepository + { + private readonly DbConnectionFactory _cf; + private readonly ILogger _log; + + public RitmoRepository(DbConnectionFactory cf, ILogger log) + { + _cf = cf; + _log = log; + } + + public async Task> GetAllAsync(string? nombreFilter) + { + var sqlBuilder = new StringBuilder("SELECT Id, Ritmo AS NombreRitmo FROM dbo.rad_dtRitmos WHERE 1=1"); + var parameters = new DynamicParameters(); + if (!string.IsNullOrWhiteSpace(nombreFilter)) + { + sqlBuilder.Append(" AND Ritmo LIKE @NombreFilterParam"); + parameters.Add("NombreFilterParam", $"%{nombreFilter}%"); + } + sqlBuilder.Append(" ORDER BY Ritmo;"); + try + { + using var connection = _cf.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener todos los Ritmos."); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int id) + { + const string sql = "SELECT Id, Ritmo AS NombreRitmo FROM dbo.rad_dtRitmos WHERE Id = @IdParam"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { IdParam = id }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error al obtener Ritmo por ID: {IdRitmo}", id); + return null; + } + } + + public async Task ExistsByNameAsync(string nombreRitmo, int? excludeId = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.rad_dtRitmos WHERE Ritmo = @NombreRitmoParam"); + var parameters = new DynamicParameters(); + parameters.Add("NombreRitmoParam", nombreRitmo); + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND Id != @ExcludeIdParam"); + parameters.Add("ExcludeIdParam", excludeId.Value); + } + try + { + using var connection = _cf.CreateConnection(); + return await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _log.LogError(ex, "Error en ExistsByNameAsync para Ritmo: {NombreRitmo}", nombreRitmo); + return true; + } + } + + public async Task IsInUseAsync(int id) + { + const string sql = "SELECT TOP 1 1 FROM dbo.rad_dtCanciones WHERE Ritmo = @IdParam"; // Columna 'Ritmo' en Canciones es FK a 'Id' en Ritmos + try + { + using var connection = _cf.CreateConnection(); + return await connection.ExecuteScalarAsync(sql, new { IdParam = id }); + } + catch (Exception ex) + { + _log.LogError(ex, "Error en IsInUseAsync para Ritmo ID: {IdRitmo}", id); + return true; + } + } + + public async Task CreateAsync(Ritmo nuevoRitmo /*, int idUsuario, IDbTransaction transaction */) + { + // Sin historial, no se necesita transacción aquí para una sola inserción. + const string sqlInsert = @" + INSERT INTO dbo.rad_dtRitmos (Ritmo) + OUTPUT INSERTED.Id, INSERTED.Ritmo AS NombreRitmo + VALUES (@NombreRitmo);"; + try + { + using var connection = _cf.CreateConnection(); + return await connection.QuerySingleAsync(sqlInsert, nuevoRitmo); + } + catch(Exception ex) + { + _log.LogError(ex, "Error al crear Ritmo: {NombreRitmo}", nuevoRitmo.NombreRitmo); + return null; + } + } + + public async Task UpdateAsync(Ritmo ritmoAActualizar /*, int idUsuario, IDbTransaction transaction */) + { + const string sqlUpdate = "UPDATE dbo.rad_dtRitmos SET Ritmo = @NombreRitmo WHERE Id = @Id;"; + try + { + using var connection = _cf.CreateConnection(); + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, ritmoAActualizar); + return rowsAffected == 1; + } + catch(Exception ex) + { + _log.LogError(ex, "Error al actualizar Ritmo ID: {IdRitmo}", ritmoAActualizar.Id); + return false; + } + } + + public async Task DeleteAsync(int id /*, int idUsuario, IDbTransaction transaction */) + { + const string sqlDelete = "DELETE FROM dbo.rad_dtRitmos WHERE Id = @IdParam"; + try + { + using var connection = _cf.CreateConnection(); + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { IdParam = id }); + return rowsAffected == 1; + } + catch(Exception ex) + { + _log.LogError(ex, "Error al eliminar Ritmo ID: {IdRitmo}", id); + return false; + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IUsuarioRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IUsuarioRepository.cs index 42b217a..8d7bd10 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IUsuarioRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/IUsuarioRepository.cs @@ -1,4 +1,5 @@ using GestionIntegral.Api.Models.Usuarios; // Para Usuario +using GestionIntegral.Api.Dtos.Usuarios.Auditoria; using System.Collections.Generic; using System.Threading.Tasks; using System.Data; @@ -16,10 +17,10 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios // Task DeleteAsync(int id, int idUsuarioModificador, IDbTransaction transaction); Task SetPasswordAsync(int userId, string newHash, string newSalt, bool debeCambiarClave, int idUsuarioModificador, IDbTransaction transaction); Task UserExistsAsync(string username, int? excludeId = null); - // Para el DTO de listado Task> GetAllWithProfileNameAsync(string? userFilter, string? nombreFilter); Task<(Usuario? Usuario, string? NombrePerfil)> GetByIdWithProfileNameAsync(int id); - + Task> GetHistorialByUsuarioIdAsync(int idUsuarioAfectado, DateTime? fechaDesde, DateTime? fechaHasta); + Task> GetAllHistorialAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModificoFilter, string? tipoModFilter); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/UsuarioRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/UsuarioRepository.cs index ef1c3d8..0730080 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/UsuarioRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Usuarios/UsuarioRepository.cs @@ -1,5 +1,6 @@ using Dapper; using GestionIntegral.Api.Models.Usuarios; +using GestionIntegral.Api.Dtos.Usuarios.Auditoria; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Data; @@ -102,7 +103,7 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios return null; } } - public async Task<(Usuario? Usuario, string? NombrePerfil)> GetByIdWithProfileNameAsync(int id) + public async Task<(Usuario? Usuario, string? NombrePerfil)> GetByIdWithProfileNameAsync(int id) { const string sql = @" SELECT u.*, p.perfil AS NombrePerfil @@ -162,7 +163,7 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios } catch (Exception ex) { - _logger.LogError(ex, "Error en UserExistsAsync para username: {Username}", username); + _logger.LogError(ex, "Error en UserExistsAsync para username: {Username}", username); return true; // Asumir que existe para prevenir duplicados } } @@ -229,13 +230,20 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios await connection.ExecuteAsync(sqlInsertHistorico, new { IdUsuarioHist = usuarioActual.Id, - UserAntHist = usuarioActual.User, UserNvoHist = usuarioAActualizar.User, // Aunque no cambiemos User, lo registramos - HabilitadaAntHist = usuarioActual.Habilitada, HabilitadaNvaHist = usuarioAActualizar.Habilitada, - SupAdminAntHist = usuarioActual.SupAdmin, SupAdminNvoHist = usuarioAActualizar.SupAdmin, - NombreAntHist = usuarioActual.Nombre, NombreNvoHist = usuarioAActualizar.Nombre, - ApellidoAntHist = usuarioActual.Apellido, ApellidoNvoHist = usuarioAActualizar.Apellido, - IdPerfilAntHist = usuarioActual.IdPerfil, IdPerfilNvoHist = usuarioAActualizar.IdPerfil, - DebeCambiarClaveAntHist = usuarioActual.DebeCambiarClave, DebeCambiarClaveNvaHist = usuarioAActualizar.DebeCambiarClave, + UserAntHist = usuarioActual.User, + UserNvoHist = usuarioAActualizar.User, // Aunque no cambiemos User, lo registramos + HabilitadaAntHist = usuarioActual.Habilitada, + HabilitadaNvaHist = usuarioAActualizar.Habilitada, + SupAdminAntHist = usuarioActual.SupAdmin, + SupAdminNvoHist = usuarioAActualizar.SupAdmin, + NombreAntHist = usuarioActual.Nombre, + NombreNvoHist = usuarioAActualizar.Nombre, + ApellidoAntHist = usuarioActual.Apellido, + ApellidoNvoHist = usuarioAActualizar.Apellido, + IdPerfilAntHist = usuarioActual.IdPerfil, + IdPerfilNvoHist = usuarioAActualizar.IdPerfil, + DebeCambiarClaveAntHist = usuarioActual.DebeCambiarClave, + DebeCambiarClaveNvaHist = usuarioAActualizar.DebeCambiarClave, IdUsuarioModHist = idUsuarioModificador, FechaModHist = DateTime.Now, TipoModHist = "Actualizado" @@ -247,9 +255,9 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios public async Task SetPasswordAsync(int userId, string newHash, string newSalt, bool debeCambiarClave, int idUsuarioModificador, IDbTransaction transaction) { - var connection = transaction.Connection!; - var usuarioActual = await connection.QuerySingleOrDefaultAsync( - "SELECT * FROM dbo.gral_Usuarios WHERE Id = @Id", new { Id = userId }, transaction); + var connection = transaction.Connection!; + var usuarioActual = await connection.QuerySingleOrDefaultAsync( + "SELECT * FROM dbo.gral_Usuarios WHERE Id = @Id", new { Id = userId }, transaction); if (usuarioActual == null) throw new KeyNotFoundException("Usuario no encontrado para cambiar contraseña."); @@ -289,5 +297,122 @@ namespace GestionIntegral.Api.Data.Repositories.Usuarios }, transaction); return rowsAffected == 1; } + + public async Task> GetHistorialByUsuarioIdAsync(int idUsuarioAfectado, DateTime? fechaDesde, DateTime? fechaHasta) + { + var sqlBuilder = new StringBuilder(@" + SELECT + h.IdHist, + h.IdUsuario AS IdUsuarioAfectado, + uPrincipal.[User] AS UserAfectado, -- DELIMITADO CON [] + h.UserAnt, h.UserNvo, + h.HabilitadaAnt, h.HabilitadaNva, + h.SupAdminAnt, h.SupAdminNvo, + h.NombreAnt, h.NombreNvo, + h.ApellidoAnt, h.ApellidoNvo, + h.IdPerfilAnt, h.IdPerfilNvo, + pAnt.perfil AS NombrePerfilAnt, + pNvo.perfil AS NombrePerfilNvo, + h.DebeCambiarClaveAnt, h.DebeCambiarClaveNva, + h.Id_UsuarioMod AS IdUsuarioModifico, + ISNULL(uMod.Nombre + ' ' + uMod.Apellido, 'Sistema') AS NombreUsuarioModifico, + h.FechaMod AS FechaModificacion, + h.TipoMod AS TipoModificacion + FROM dbo.gral_Usuarios_H h + INNER JOIN dbo.gral_Usuarios uPrincipal ON h.IdUsuario = uPrincipal.Id -- No necesita delimitador aquí si 'Id' es el nombre de la columna PK en gral_Usuarios + LEFT JOIN dbo.gral_Usuarios uMod ON h.Id_UsuarioMod = uMod.Id + LEFT JOIN dbo.gral_Perfiles pAnt ON h.IdPerfilAnt = pAnt.id + LEFT JOIN dbo.gral_Perfiles pNvo ON h.IdPerfilNvo = pNvo.id + WHERE h.IdUsuario = @IdUsuarioAfectadoParam"); + + var parameters = new DynamicParameters(); + parameters.Add("IdUsuarioAfectadoParam", idUsuarioAfectado); + + if (fechaDesde.HasValue) + { + sqlBuilder.Append(" AND h.FechaMod >= @FechaDesdeParam"); + parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); + } + if (fechaHasta.HasValue) + { + sqlBuilder.Append(" AND h.FechaMod <= @FechaHastaParam"); + parameters.Add("FechaHastaParam", fechaHasta.Value.Date.AddDays(1).AddTicks(-1)); // Hasta el final del día + } + sqlBuilder.Append(" ORDER BY h.FechaMod DESC, h.IdHist DESC;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener historial para Usuario ID: {IdUsuarioAfectado}", idUsuarioAfectado); + return Enumerable.Empty(); + } + } + + public async Task> GetAllHistorialAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModificoFilter, string? tipoModFilter) + { + var sqlBuilder = new StringBuilder(@" + SELECT + h.IdHist, + h.IdUsuario AS IdUsuarioAfectado, + uPrincipal.[User] AS UserAfectado, -- DELIMITADO CON [] + h.UserAnt, h.UserNvo, + h.HabilitadaAnt, h.HabilitadaNva, + h.SupAdminAnt, h.SupAdminNvo, + h.NombreAnt, h.NombreNvo, + h.ApellidoAnt, h.ApellidoNvo, + h.IdPerfilAnt, h.IdPerfilNvo, + pAnt.perfil AS NombrePerfilAnt, + pNvo.perfil AS NombrePerfilNvo, + h.DebeCambiarClaveAnt, h.DebeCambiarClaveNva, + h.Id_UsuarioMod AS IdUsuarioModifico, + ISNULL(uMod.Nombre + ' ' + uMod.Apellido, 'Sistema') AS NombreUsuarioModifico, + h.FechaMod AS FechaModificacion, + h.TipoMod AS TipoModificacion + FROM dbo.gral_Usuarios_H h + INNER JOIN dbo.gral_Usuarios uPrincipal ON h.IdUsuario = uPrincipal.Id + LEFT JOIN dbo.gral_Usuarios uMod ON h.Id_UsuarioMod = uMod.Id + LEFT JOIN dbo.gral_Perfiles pAnt ON h.IdPerfilAnt = pAnt.id + LEFT JOIN dbo.gral_Perfiles pNvo ON h.IdPerfilNvo = pNvo.id + WHERE 1=1"); + + var parameters = new DynamicParameters(); + + if (fechaDesde.HasValue) + { + sqlBuilder.Append(" AND h.FechaMod >= @FechaDesdeParam"); + parameters.Add("FechaDesdeParam", fechaDesde.Value.Date); + } + if (fechaHasta.HasValue) + { + sqlBuilder.Append(" AND h.FechaMod <= @FechaHastaParam"); + parameters.Add("FechaHastaParam", fechaHasta.Value.Date.AddDays(1).AddTicks(-1)); + } + if (idUsuarioModificoFilter.HasValue) + { + sqlBuilder.Append(" AND h.Id_UsuarioMod = @IdUsuarioModFilterParam"); + parameters.Add("IdUsuarioModFilterParam", idUsuarioModificoFilter.Value); + } + if (!string.IsNullOrWhiteSpace(tipoModFilter)) + { + sqlBuilder.Append(" AND h.TipoMod LIKE @TipoModFilterParam"); + parameters.Add("TipoModFilterParam", $"%{tipoModFilter}%"); + } + sqlBuilder.Append(" ORDER BY h.FechaMod DESC, h.IdHist DESC;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todo el historial de Usuarios."); + return Enumerable.Empty(); + } + } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj b/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj index 378870f..aef98f8 100644 --- a/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj +++ b/Backend/GestionIntegral.Api/GestionIntegral.Api.csproj @@ -11,6 +11,7 @@ + diff --git a/Backend/GestionIntegral.Api/Models/Contables/NotaCreditoDebito.cs b/Backend/GestionIntegral.Api/Models/Contables/NotaCreditoDebito.cs new file mode 100644 index 0000000..c39195c --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Contables/NotaCreditoDebito.cs @@ -0,0 +1,16 @@ +using System; +namespace GestionIntegral.Api.Models.Contables +{ + public class NotaCreditoDebito // Corresponde a cue_CreditosDebitos + { + public int IdNota { get; set; } // Id_Nota (PK, Identity) + public string Destino { get; set; } = string.Empty; // "Distribuidores" o "Canillas" + public int IdDestino { get; set; } // Id del distribuidor o canillita + public string? Referencia { get; set; } // varchar(50), NULL + public string Tipo { get; set; } = string.Empty; // "Debito" o "Credito" + public DateTime Fecha { get; set; } + public decimal Monto { get; set; } + public string? Observaciones { get; set; } // varchar(250), NULL + public int IdEmpresa { get; set; } // Empresa a la que corresponde el saldo afectado + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Contables/NotaCreditoDebitoHistorico.cs b/Backend/GestionIntegral.Api/Models/Contables/NotaCreditoDebitoHistorico.cs new file mode 100644 index 0000000..db982e9 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Contables/NotaCreditoDebitoHistorico.cs @@ -0,0 +1,20 @@ +using System; +namespace GestionIntegral.Api.Models.Contables +{ + public class NotaCreditoDebitoHistorico // Corresponde a cue_CreditosDebitos_H + { + // No hay PK explícita en _H + public int Id_Nota { get; set; } // Coincide con columna en _H + public string Destino { get; set; } = string.Empty; + public int Id_Destino { get; set; } + public string? Referencia { get; set; } + public string Tipo { get; set; } = string.Empty; + public DateTime Fecha { get; set; } + public decimal Monto { get; set; } + public string? Observaciones { get; set; } + public int Id_Empresa { get; set; } + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Contables/PagoDistribuidor.cs b/Backend/GestionIntegral.Api/Models/Contables/PagoDistribuidor.cs new file mode 100644 index 0000000..0864026 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Contables/PagoDistribuidor.cs @@ -0,0 +1,16 @@ +using System; +namespace GestionIntegral.Api.Models.Contables +{ + public class PagoDistribuidor // Corresponde a cue_PagosDistribuidor + { + public int IdPago { get; set; } // Id_Pago (PK, Identity) + public int IdDistribuidor { get; set; } + public DateTime Fecha { get; set; } + public string TipoMovimiento { get; set; } = string.Empty; // "Recibido" o "Realizado" + public int Recibo { get; set; } // Nro de recibo + public decimal Monto { get; set; } + public int IdTipoPago { get; set; } // FK a cue_dtTipopago + public string? Detalle { get; set; } + public int IdEmpresa { get; set; } // Empresa a la que corresponde el saldo afectado + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Contables/PagoDistribuidorHistorico.cs b/Backend/GestionIntegral.Api/Models/Contables/PagoDistribuidorHistorico.cs new file mode 100644 index 0000000..fd1a663 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Contables/PagoDistribuidorHistorico.cs @@ -0,0 +1,20 @@ +using System; +namespace GestionIntegral.Api.Models.Contables +{ + public class PagoDistribuidorHistorico // Corresponde a cue_PagosDistribuidor_H + { + // No hay PK explícita en _H + public int Id_Pago { get; set; } // Coincide con columna en _H + public int Id_Distribuidor { get; set; } + public DateTime Fecha { get; set; } + public string TipoMovimiento { get; set; } = string.Empty; + public int Recibo { get; set; } + public decimal Monto { get; set; } + public int Id_TipoPago { get; set; } + public string? Detalle { get; set; } + public int Id_Empresa { get; set; } + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/ControlDevoluciones.cs b/Backend/GestionIntegral.Api/Models/Distribucion/ControlDevoluciones.cs new file mode 100644 index 0000000..82e5c67 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/ControlDevoluciones.cs @@ -0,0 +1,14 @@ +using System; +namespace GestionIntegral.Api.Models.Distribucion +{ + public class ControlDevoluciones // Corresponde a dist_dtCtrlDevoluciones + { + public int IdControl { get; set; } // Id_Control (PK, Identity) + public int IdEmpresa { get; set; } + public DateTime Fecha { get; set; } + public int Entrada { get; set; } // Cantidad total de ejemplares que ingresaron como devolución + public int Sobrantes { get; set; } + public string? Detalle { get; set; } + public int SinCargo { get; set; } // DEFAULT 0 NOT NULL + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/ControlDevolucionesHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/ControlDevolucionesHistorico.cs new file mode 100644 index 0000000..d793a64 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/ControlDevolucionesHistorico.cs @@ -0,0 +1,18 @@ +using System; +namespace GestionIntegral.Api.Models.Distribucion +{ + public class ControlDevolucionesHistorico // Corresponde a dist_dtCtrlDevoluciones_H + { + // No hay PK explícita en _H + public int Id_Control { get; set; } // Coincide con columna en _H + public int Id_Empresa { get; set; } + public DateTime Fecha { get; set; } + public int Entrada { get; set; } + public int Sobrantes { get; set; } + public string? Detalle { get; set; } + public int SinCargo { get; set; } + public int Id_Usuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/EntradaSalidaCanilla.cs b/Backend/GestionIntegral.Api/Models/Distribucion/EntradaSalidaCanilla.cs new file mode 100644 index 0000000..8dba773 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/EntradaSalidaCanilla.cs @@ -0,0 +1,20 @@ +using System; +namespace GestionIntegral.Api.Models.Distribucion +{ + public class EntradaSalidaCanilla // Corresponde a dist_EntradasSalidasCanillas + { + public int IdParte { get; set; } + public int IdPublicacion { get; set; } + public int IdCanilla { get; set; } + public DateTime Fecha { get; set; } + public int CantSalida { get; set; } + public int CantEntrada { get; set; } + public int IdPrecio { get; set; } // FK a dist_Precios + public int IdRecargo { get; set; } // FK a dist_RecargoZona (0 si no aplica) + public int IdPorcMon { get; set; } // FK a dist_PorcMonPagoCanilla (0 si no aplica) + public string? Observacion { get; set; } + public bool Liquidado { get; set; } + public DateTime? FechaLiquidado { get; set; } + public int? UserLiq { get; set; } // Usuario que liquidó + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Contables/CreateNotaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Contables/CreateNotaDto.cs new file mode 100644 index 0000000..c28050c --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Contables/CreateNotaDto.cs @@ -0,0 +1,34 @@ +using System; +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Contables +{ + public class CreateNotaDto + { + [Required(ErrorMessage = "El tipo de destino es obligatorio ('Distribuidores' o 'Canillas').")] + [RegularExpression("^(Distribuidores|Canillas)$", ErrorMessage = "Destino debe ser 'Distribuidores' o 'Canillas'.")] + public string Destino { get; set; } = string.Empty; + + [Required(ErrorMessage = "El destinatario es obligatorio.")] + [Range(1, int.MaxValue)] + public int IdDestino { get; set; } + + [StringLength(50)] + public string? Referencia { get; set; } + + [Required(ErrorMessage = "El tipo de nota es obligatorio ('Debito' o 'Credito').")] + [RegularExpression("^(Debito|Credito)$", ErrorMessage = "Tipo debe ser 'Debito' o 'Credito'.")] + public string Tipo { get; set; } = string.Empty; + + [Required] + public DateTime Fecha { get; set; } + + [Required, Range(0.01, (double)decimal.MaxValue, ErrorMessage = "El monto debe ser mayor a cero.")] + public decimal Monto { get; set; } + + [StringLength(250)] + public string? Observaciones { get; set; } + + [Required] + public int IdEmpresa { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Contables/CreatePagoDistribuidorDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Contables/CreatePagoDistribuidorDto.cs new file mode 100644 index 0000000..e351391 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Contables/CreatePagoDistribuidorDto.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Contables +{ + public class CreatePagoDistribuidorDto + { + [Required] + public int IdDistribuidor { get; set; } + [Required] + public DateTime Fecha { get; set; } + [Required] + [RegularExpression("^(Recibido|Realizado)$", ErrorMessage = "Tipo de movimiento debe ser 'Recibido' o 'Realizado'.")] + public string TipoMovimiento { get; set; } = string.Empty; + [Required, Range(1, int.MaxValue)] + public int Recibo { get; set; } // Nro de Recibo + [Required, Range(0.01, (double)decimal.MaxValue, ErrorMessage = "El monto debe ser mayor a cero.")] + public decimal Monto { get; set; } + [Required] + public int IdTipoPago { get; set; } + [StringLength(150)] + public string? Detalle { get; set; } + [Required] + public int IdEmpresa { get; set; } // Empresa cuyo saldo se verá afectado + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Contables/NotaCreditoDebitoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Contables/NotaCreditoDebitoDto.cs new file mode 100644 index 0000000..b707104 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Contables/NotaCreditoDebitoDto.cs @@ -0,0 +1,17 @@ +namespace GestionIntegral.Api.Dtos.Contables +{ + public class NotaCreditoDebitoDto + { + public int IdNota { get; set; } + public string Destino { get; set; } = string.Empty; // "Distribuidores", "Canillas" + public int IdDestino { get; set; } + public string NombreDestinatario { get; set; } = string.Empty; // Nombre del Distribuidor o Canillita + public string? Referencia { get; set; } + public string Tipo { get; set; } = string.Empty; // "Debito", "Credito" + public string Fecha { get; set; } = string.Empty; // yyyy-MM-dd + public decimal Monto { get; set; } + public string? Observaciones { get; set; } + public int IdEmpresa { get; set; } + public string NombreEmpresa { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Contables/PagoDistribuidorDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Contables/PagoDistribuidorDto.cs new file mode 100644 index 0000000..5285e26 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Contables/PagoDistribuidorDto.cs @@ -0,0 +1,18 @@ +namespace GestionIntegral.Api.Dtos.Contables +{ + public class PagoDistribuidorDto + { + public int IdPago { get; set; } + public int IdDistribuidor { get; set; } + public string NombreDistribuidor { get; set; } = string.Empty; + public string Fecha { get; set; } = string.Empty; // yyyy-MM-dd + public string TipoMovimiento { get; set; } = string.Empty; // "Recibido" / "Realizado" + public int Recibo { get; set; } + public decimal Monto { get; set; } + public int IdTipoPago { get; set; } + public string NombreTipoPago { get; set; } = string.Empty; + public string? Detalle { get; set; } + public int IdEmpresa { get; set; } + public string NombreEmpresa { get; set; } = string.Empty; // Empresa del saldo afectado + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Contables/UpdateNotaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Contables/UpdateNotaDto.cs new file mode 100644 index 0000000..270ad81 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Contables/UpdateNotaDto.cs @@ -0,0 +1,15 @@ +// Para las notas, la edición es limitada. Si se comete un error grave, se anula y se crea una nueva. +// Podríamos permitir cambiar Monto y Observaciones. +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Contables +{ + public class UpdateNotaDto + { + [Required, Range(0.01, (double)decimal.MaxValue, ErrorMessage = "El monto debe ser mayor a cero.")] + public decimal Monto { get; set; } + + [StringLength(250)] + public string? Observaciones { get; set; } + // No se permite cambiar Destino, IdDestino, Tipo, Fecha, Referencia, IdEmpresa de una nota existente. + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Contables/UpdatePagoDistribuidorDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Contables/UpdatePagoDistribuidorDto.cs new file mode 100644 index 0000000..2d81eb8 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Contables/UpdatePagoDistribuidorDto.cs @@ -0,0 +1,17 @@ +// La edición de un pago puede ser delicada por la afectación de saldos. +// Podríamos permitir cambiar Monto, TipoPago, Detalle. +// Cambiar Fecha, Distribuidor, Empresa, TipoMovimiento, Recibo podría requerir anular y recrear. +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Contables +{ + public class UpdatePagoDistribuidorDto + { + [Required, Range(0.01, (double)decimal.MaxValue, ErrorMessage = "El monto debe ser mayor a cero.")] + public decimal Monto { get; set; } + [Required] + public int IdTipoPago { get; set; } + [StringLength(150)] + public string? Detalle { get; set; } + // Los campos IdDistribuidor, Fecha, TipoMovimiento, Recibo, IdEmpresa no se cambian aquí. + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/ControlDevolucionesDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/ControlDevolucionesDto.cs new file mode 100644 index 0000000..f108811 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/ControlDevolucionesDto.cs @@ -0,0 +1,14 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class ControlDevolucionesDto + { + public int IdControl { get; set; } + public int IdEmpresa { get; set; } + public string NombreEmpresa { get; set; } = string.Empty; + public string Fecha { get; set; } = string.Empty; // yyyy-MM-dd + public int Entrada { get; set; } + public int Sobrantes { get; set; } + public string? Detalle { get; set; } + public int SinCargo { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto.cs new file mode 100644 index 0000000..f940e5f --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreateBulkEntradaSalidaCanillaDto + { + [Required(ErrorMessage = "El ID del canillita es obligatorio.")] + public int IdCanilla { get; set; } + + [Required(ErrorMessage = "La fecha del movimiento es obligatoria.")] + public DateTime Fecha { get; set; } // Fecha común para todos los ítems + + [Required(ErrorMessage = "Debe haber al menos un ítem de movimiento.")] + [MinLength(1, ErrorMessage = "Debe agregar al menos una publicación.")] + public List Items { get; set; } = new List(); + + // Validar que no haya publicaciones duplicadas en la lista de items + [CustomValidation(typeof(CreateBulkEntradaSalidaCanillaDto), nameof(ValidateNoDuplicatePublications))] + public string? DuplicateError { get; set; } + + public static ValidationResult? ValidateNoDuplicatePublications(CreateBulkEntradaSalidaCanillaDto dto, ValidationContext context) + { + if (dto.Items != null) + { + var duplicatePublications = dto.Items + .GroupBy(item => item.IdPublicacion) + .Where(group => group.Count() > 1) + .Select(group => group.Key) + .ToList(); + + if (duplicatePublications.Any()) + { + return new ValidationResult($"No puede agregar la misma publicación varias veces. Publicaciones duplicadas: {string.Join(", ", duplicatePublications)}"); + } + } + return ValidationResult.Success; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateControlDevolucionesDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateControlDevolucionesDto.cs new file mode 100644 index 0000000..fdc2c96 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateControlDevolucionesDto.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreateControlDevolucionesDto + { + [Required] + public int IdEmpresa { get; set; } + [Required] + public DateTime Fecha { get; set; } + [Required, Range(0, int.MaxValue)] // Entrada puede ser 0 si solo hay sobrantes/sin cargo + public int Entrada { get; set; } + [Required, Range(0, int.MaxValue)] + public int Sobrantes { get; set; } + [StringLength(250)] + public string? Detalle { get; set; } + [Required, Range(0, int.MaxValue)] + public int SinCargo { get; set; } = 0; // Default 0 + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateEntradaSalidaCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateEntradaSalidaCanillaDto.cs new file mode 100644 index 0000000..d18f52d --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/CreateEntradaSalidaCanillaDto.cs @@ -0,0 +1,34 @@ +using System; +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class CreateEntradaSalidaCanillaDto + { + [Required] + public int IdPublicacion { get; set; } + [Required] + public int IdCanilla { get; set; } + [Required] + public DateTime Fecha { get; set; } // Fecha del movimiento (retiro/devolución) + [Required, Range(0, int.MaxValue)] // Puede retirar 0 si es solo para registrar devolución + public int CantSalida { get; set; } + [Required, Range(0, int.MaxValue)] + public int CantEntrada { get; set; } + [StringLength(150)] + public string? Observacion { get; set; } + // Liquidado, FechaLiquidado, UserLiq se manejan por una acción de "Liquidar" separada. + // IdPrecio, IdRecargo, IdPorcMon se determinan en el backend. + + [CustomValidation(typeof(CreateEntradaSalidaCanillaDto), nameof(ValidateCantidades))] + public string? CantidadesError { get; set; } // Dummy para validación + + public static ValidationResult? ValidateCantidades(CreateEntradaSalidaCanillaDto dto, ValidationContext context) + { + if (dto.CantEntrada > dto.CantSalida) + { + return new ValidationResult("La cantidad de entrada (devolución) no puede ser mayor a la cantidad de salida (retiro)."); + } + return ValidationResult.Success; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EntradaSalidaCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EntradaSalidaCanillaDto.cs new file mode 100644 index 0000000..c4e7be6 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EntradaSalidaCanillaDto.cs @@ -0,0 +1,26 @@ +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class EntradaSalidaCanillaDto + { + public int IdParte { get; set; } + public int IdPublicacion { get; set; } + public string NombrePublicacion { get; set; } = string.Empty; + public int IdCanilla { get; set; } + public string NomApeCanilla { get; set; } = string.Empty; + public bool CanillaEsAccionista { get; set; } + public string Fecha { get; set; } = string.Empty; // yyyy-MM-dd + public int CantSalida { get; set; } + public int CantEntrada { get; set; } + public int Vendidos { get; set; } // Calculado + public string? Observacion { get; set; } + public bool Liquidado { get; set; } + public string? FechaLiquidado { get; set; } // yyyy-MM-dd + public int? UserLiq { get; set; } + public string? NombreUserLiq { get; set; } // Para mostrar en UI + public decimal MontoARendir { get; set; } // Calculado por el backend + public decimal PrecioUnitarioAplicado { get; set; } // Info para UI + public decimal RecargoAplicado { get; set; } // Info para UI + public decimal PorcentajeOMontoCanillaAplicado { get; set; } // Info para UI + public bool EsPorcentajeCanilla { get; set; } // Info para UI + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EntradaSalidaCanillaItemDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EntradaSalidaCanillaItemDto.cs new file mode 100644 index 0000000..de4d427 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/EntradaSalidaCanillaItemDto.cs @@ -0,0 +1,35 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class EntradaSalidaCanillaItemDto + { + [Required(ErrorMessage = "El ID de la publicación es obligatorio.")] + public int IdPublicacion { get; set; } + + [Required(ErrorMessage = "La cantidad de salida es obligatoria.")] + [Range(0, int.MaxValue, ErrorMessage = "La cantidad de salida debe ser un número positivo o cero.")] + public int CantSalida { get; set; } + + [Required(ErrorMessage = "La cantidad de entrada es obligatoria.")] + [Range(0, int.MaxValue, ErrorMessage = "La cantidad de entrada debe ser un número positivo o cero.")] + public int CantEntrada { get; set; } + + [StringLength(150)] + public string? Observacion { get; set; } // Observación por línea + + // Validar que CantEntrada no sea mayor que CantSalida + [CustomValidation(typeof(EntradaSalidaCanillaItemDto), nameof(ValidateCantidades))] + public string? CantidadesError { get; set; } + + public static ValidationResult? ValidateCantidades(EntradaSalidaCanillaItemDto item, ValidationContext context) + { + if (item.CantEntrada > item.CantSalida) + { + return new ValidationResult("La cantidad de entrada (devolución) no puede ser mayor a la cantidad de salida (retiro) para esta publicación.", new[] { nameof(CantEntrada) }); + } + return ValidationResult.Success; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/LiquidarMovimientosCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/LiquidarMovimientosCanillaDto.cs new file mode 100644 index 0000000..d8f164a --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/LiquidarMovimientosCanillaDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class LiquidarMovimientosCanillaRequestDto // Para liquidar uno o más movimientos + { + [Required] + [MinLength(1)] + public List IdsPartesALiquidar { get; set; } = new List(); + [Required] + public DateTime FechaLiquidacion { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateControlDevolucionesDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateControlDevolucionesDto.cs new file mode 100644 index 0000000..18fda75 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateControlDevolucionesDto.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdateControlDevolucionesDto + { + // IdEmpresa y Fecha no deberían cambiar para un registro existente. Si cambian, es un nuevo registro. + [Required, Range(0, int.MaxValue)] + public int Entrada { get; set; } + [Required, Range(0, int.MaxValue)] + public int Sobrantes { get; set; } + [StringLength(250)] + public string? Detalle { get; set; } + [Required, Range(0, int.MaxValue)] + public int SinCargo { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateEntradaSalidaCanillaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateEntradaSalidaCanillaDto.cs new file mode 100644 index 0000000..e8c9dfe --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Distribucion/UpdateEntradaSalidaCanillaDto.cs @@ -0,0 +1,27 @@ +// Similar a E/S Distribuidores, la edición es limitada para no afectar cálculos complejos ya hechos. +// Principalmente para corregir cantidades si aún no está liquidado. +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Distribucion +{ + public class UpdateEntradaSalidaCanillaDto + { + [Required, Range(0, int.MaxValue)] + public int CantSalida { get; set; } + [Required, Range(0, int.MaxValue)] + public int CantEntrada { get; set; } + [StringLength(150)] + public string? Observacion { get; set; } + + [CustomValidation(typeof(UpdateEntradaSalidaCanillaDto), nameof(ValidateCantidades))] + public string? CantidadesError { get; set; } // Dummy para validación + + public static ValidationResult? ValidateCantidades(UpdateEntradaSalidaCanillaDto dto, ValidationContext context) + { + if (dto.CantEntrada > dto.CantSalida) + { + return new ValidationResult("La cantidad de entrada no puede ser mayor a la de salida."); + } + return ValidationResult.Success; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Radios/CancionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Radios/CancionDto.cs new file mode 100644 index 0000000..772435a --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Radios/CancionDto.cs @@ -0,0 +1,18 @@ +namespace GestionIntegral.Api.Dtos.Radios +{ + public class CancionDto + { + public int Id { get; set; } + public string? Tema { get; set; } + public string? CompositorAutor { get; set; } + public string? Interprete { get; set; } + public string? Sello { get; set; } + public string? Placa { get; set; } + public int? Pista { get; set; } + public string? Introduccion { get; set; } + public int? IdRitmo { get; set; } // Renombrado de Ritmo a IdRitmo para claridad en DTO + public string? NombreRitmo { get; set; } // Para mostrar en UI + public string? Formato { get; set; } + public string? Album { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Radios/CancionEnListaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Radios/CancionEnListaDto.cs new file mode 100644 index 0000000..21dab35 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Radios/CancionEnListaDto.cs @@ -0,0 +1,17 @@ +namespace GestionIntegral.Api.Dtos.Radios +{ + public class CancionEnListaDto // Lo que se va a escribir en cada fila del Excel + { + public string? Tema { get; set; } + public string? Interprete { get; set; } + public string? CompositorAutor { get; set; } + public string? Album { get; set; } + public string? Sello { get; set; } + public string? Placa { get; set; } // Nro de Catálogo / Identificador + public int? Pista { get; set; } + public string? Ritmo { get; set; } // Nombre del ritmo + public string? Formato { get; set; } + public string? Introduccion { get; set; } // Para saber duración o cues + // Podrías añadir campos como "HoraProgramada" si la generación es por franjas + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Radios/CreateCancionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Radios/CreateCancionDto.cs new file mode 100644 index 0000000..1c31159 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Radios/CreateCancionDto.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Radios +{ + public class CreateCancionDto + { + [StringLength(255)] + public string? Tema { get; set; } // Tema podría ser el campo más importante + + [StringLength(255)] + public string? CompositorAutor { get; set; } + + [StringLength(255)] + public string? Interprete { get; set; } + + [StringLength(255)] + public string? Sello { get; set; } + + [StringLength(255)] + public string? Placa { get; set; } + + public int? Pista { get; set; } + + [StringLength(255)] + public string? Introduccion { get; set; } + + public int? IdRitmo { get; set; } // FK + + [StringLength(255)] + public string? Formato { get; set; } + + [StringLength(255)] + public string? Album { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Radios/CreateRitmoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Radios/CreateRitmoDto.cs new file mode 100644 index 0000000..1fd6b14 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Radios/CreateRitmoDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Radios +{ + public class CreateRitmoDto + { + [StringLength(255, ErrorMessage = "El nombre del ritmo no puede exceder los 255 caracteres.")] + // Aunque la BD permite NULL, para crear usualmente se requeriría un nombre. + // Si se permite crear ritmos sin nombre, quitar [Required] + [Required(ErrorMessage = "El nombre del ritmo es obligatorio.")] + public string NombreRitmo { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Radios/GenerarListaRadioRequestDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Radios/GenerarListaRadioRequestDto.cs new file mode 100644 index 0000000..e0ede4f --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Radios/GenerarListaRadioRequestDto.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Radios +{ + public class GenerarListaRadioRequestDto + { + [Required(ErrorMessage = "El mes es obligatorio.")] + [Range(1, 12, ErrorMessage = "El mes debe estar entre 1 y 12.")] + public int Mes { get; set; } + + [Required(ErrorMessage = "El año es obligatorio.")] + [Range(2000, 2999, ErrorMessage = "El año debe ser un valor válido (ej. 2024).")] // Ajusta el rango si es necesario + public int Anio { get; set; } + + [Required(ErrorMessage = "La institución es obligatoria.")] + [RegularExpression("^(AADI|SADAIC)$", ErrorMessage = "La institución debe ser 'AADI' o 'SADAIC'.")] + public string Institucion { get; set; } = string.Empty; + + [Required(ErrorMessage = "La radio es obligatoria.")] + [RegularExpression("^(FM 99.1|FM 100.3)$", ErrorMessage = "La radio debe ser 'FM 99.1' o 'FM 100.3'.")] + public string Radio { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Radios/ProgramacionHorariaExcelDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Radios/ProgramacionHorariaExcelDto.cs new file mode 100644 index 0000000..90b6a0e --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Radios/ProgramacionHorariaExcelDto.cs @@ -0,0 +1,16 @@ +namespace GestionIntegral.Api.Dtos.Radios +{ + public class ProgramacionHorariaExcelDto + { + public int Dia { get; set; } + public int Hora { get; set; } + public string? TituloObra { get; set; } // Mapeado desde Cancion.Tema + public string? CompositorAutor { get; set; } + public string? Interprete { get; set; } + public string? Sello { get; set; } + public string? Album { get; set; } + // No se incluyen Pista, Introducción, Formato, Placa, NombreRitmo + // porque el Excel original de VB.NET no los tenía. + // Si se decide mantenerlos en el futuro, se podrían añadir aquí. + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Radios/RitmoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Radios/RitmoDto.cs new file mode 100644 index 0000000..28c1a0b --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Radios/RitmoDto.cs @@ -0,0 +1,8 @@ +namespace GestionIntegral.Api.Dtos.Radios +{ + public class RitmoDto + { + public int Id { get; set; } + public string? NombreRitmo { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Radios/UpdateCancionDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Radios/UpdateCancionDto.cs new file mode 100644 index 0000000..5426e2a --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Radios/UpdateCancionDto.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Radios +{ + public class UpdateCancionDto + { + [StringLength(255)] + public string? Tema { get; set; } + + [StringLength(255)] + public string? CompositorAutor { get; set; } + + [StringLength(255)] + public string? Interprete { get; set; } + + [StringLength(255)] + public string? Sello { get; set; } + + [StringLength(255)] + public string? Placa { get; set; } + + public int? Pista { get; set; } + + [StringLength(255)] + public string? Introduccion { get; set; } + + public int? IdRitmo { get; set; } + + [StringLength(255)] + public string? Formato { get; set; } + + [StringLength(255)] + public string? Album { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Radios/UpdateRitmoDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Radios/UpdateRitmoDto.cs new file mode 100644 index 0000000..8124eb5 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Radios/UpdateRitmoDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Radios +{ + public class UpdateRitmoDto + { + [StringLength(255, ErrorMessage = "El nombre del ritmo no puede exceder los 255 caracteres.")] + [Required(ErrorMessage = "El nombre del ritmo es obligatorio.")] + public string NombreRitmo { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/Auditoria/UsuarioHistorialDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/Auditoria/UsuarioHistorialDto.cs new file mode 100644 index 0000000..d966a84 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Usuarios/Auditoria/UsuarioHistorialDto.cs @@ -0,0 +1,35 @@ +using System; + +namespace GestionIntegral.Api.Dtos.Usuarios.Auditoria +{ + public class UsuarioHistorialDto + { + public int IdHist { get; set; } // PK de la tabla gral_Usuarios_H + public int IdUsuarioAfectado { get; set; } // IdUsuario (el usuario cuyos datos cambiaron) + public string UserAfectado { get; set; } = string.Empty; // UserNvo (username del usuario afectado) + + // Campos del historial + public string? UserAnt { get; set; } + public string UserNvo { get; set; } = string.Empty; + public bool? HabilitadaAnt { get; set; } + public bool HabilitadaNva { get; set; } + public bool? SupAdminAnt { get; set; } + public bool SupAdminNvo { get; set; } + public string? NombreAnt { get; set; } + public string NombreNvo { get; set; } = string.Empty; + public string? ApellidoAnt { get; set; } + public string ApellidoNvo { get; set; } = string.Empty; + public int? IdPerfilAnt { get; set; } + public int IdPerfilNvo { get; set; } + public string? NombrePerfilAnt { get; set; } // Join con gral_Perfiles + public string NombrePerfilNvo { get; set; } = string.Empty; // Join con gral_Perfiles + public bool? DebeCambiarClaveAnt { get; set; } + public bool DebeCambiarClaveNva { get; set; } + + // Auditoría del registro de historial + public int IdUsuarioModifico { get; set; } // Id_UsuarioMod + public string NombreUsuarioModifico { get; set; } = string.Empty; // Nombre del usuario que hizo el cambio + public DateTime FechaModificacion { get; set; } // FechaMod + public string TipoModificacion { get; set; } = string.Empty; // TipoMod + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Radios/Cancion.cs b/Backend/GestionIntegral.Api/Models/Radios/Cancion.cs new file mode 100644 index 0000000..c5f619d --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Radios/Cancion.cs @@ -0,0 +1,17 @@ +namespace GestionIntegral.Api.Models.Radios +{ + public class Cancion // Corresponde a rad_dtCanciones + { + public int Id { get; set; } // Id (PK, Identity) + public string? Tema { get; set; } + public string? CompositorAutor { get; set; } + public string? Interprete { get; set; } + public string? Sello { get; set; } + public string? Placa { get; set; } + public int? Pista { get; set; } + public string? Introduccion { get; set; } + public int? Ritmo { get; set; } // FK a rad_dtRitmos.Id + public string? Formato { get; set; } + public string? Album { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Radios/Ritmo.cs b/Backend/GestionIntegral.Api/Models/Radios/Ritmo.cs new file mode 100644 index 0000000..2cc8fd9 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Radios/Ritmo.cs @@ -0,0 +1,8 @@ +namespace GestionIntegral.Api.Models.Radios +{ + public class Ritmo // Corresponde a rad_dtRitmos + { + public int Id { get; set; } // Id (PK, Identity) + public string? NombreRitmo { get; set; } // Columna "Ritmo" (nvarchar(255), NULL) - Renombrado para claridad + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Program.cs b/Backend/GestionIntegral.Api/Program.cs index 960ddbc..9a59f41 100644 --- a/Backend/GestionIntegral.Api/Program.cs +++ b/Backend/GestionIntegral.Api/Program.cs @@ -4,9 +4,11 @@ using System.Text; using GestionIntegral.Api.Data; using GestionIntegral.Api.Services.Contables; using GestionIntegral.Api.Services.Distribucion; +using GestionIntegral.Api.Services.Radios; using GestionIntegral.Api.Data.Repositories.Contables; using GestionIntegral.Api.Data.Repositories.Distribucion; using GestionIntegral.Api.Data.Repositories.Impresion; +using GestionIntegral.Api.Data.Repositories.Radios; using GestionIntegral.Api.Services.Impresion; using GestionIntegral.Api.Services.Usuarios; using GestionIntegral.Api.Data.Repositories.Usuarios; @@ -65,6 +67,19 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // --- Configuración de Autenticación JWT --- var jwtSettings = builder.Configuration.GetSection("Jwt"); diff --git a/Backend/GestionIntegral.Api/Services/Contables/INotaCreditoDebitoService.cs b/Backend/GestionIntegral.Api/Services/Contables/INotaCreditoDebitoService.cs new file mode 100644 index 0000000..3b67052 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Contables/INotaCreditoDebitoService.cs @@ -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> ObtenerTodosAsync( + DateTime? fechaDesde, DateTime? fechaHasta, + string? destino, int? idDestino, int? idEmpresa, string? tipoNota); + + Task 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); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Contables/IPagoDistribuidorService.cs b/Backend/GestionIntegral.Api/Services/Contables/IPagoDistribuidorService.cs new file mode 100644 index 0000000..5fa6438 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Contables/IPagoDistribuidorService.cs @@ -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> ObtenerTodosAsync( + DateTime? fechaDesde, DateTime? fechaHasta, + int? idDistribuidor, int? idEmpresa, string? tipoMovimiento); + + Task 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); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Contables/NotaCreditoDebitoService.cs b/Backend/GestionIntegral.Api/Services/Contables/NotaCreditoDebitoService.cs new file mode 100644 index 0000000..8b27f5d --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Contables/NotaCreditoDebitoService.cs @@ -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 _logger; + + public NotaCreditoDebitoService( + INotaCreditoDebitoRepository notaRepo, + IDistribuidorRepository distribuidorRepo, + ICanillaRepository canillaRepo, + IEmpresaRepository empresaRepo, + ISaldoRepository saldoRepo, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _notaRepo = notaRepo; + _distribuidorRepo = distribuidorRepo; + _canillaRepo = canillaRepo; + _empresaRepo = empresaRepo; + _saldoRepo = saldoRepo; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private async Task 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> 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(); + foreach (var nota in notas) + { + dtos.Add(await MapToDto(nota)); + } + return dtos; + } + + public async Task 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}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs b/Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs new file mode 100644 index 0000000..6552435 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Contables/PagoDistribuidorService.cs @@ -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 _logger; + + public PagoDistribuidorService( + IPagoDistribuidorRepository pagoRepo, + IDistribuidorRepository distribuidorRepo, + ITipoPagoRepository tipoPagoRepo, + IEmpresaRepository empresaRepo, + ISaldoRepository saldoRepo, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _pagoRepo = pagoRepo; + _distribuidorRepo = distribuidorRepo; + _tipoPagoRepo = tipoPagoRepo; + _empresaRepo = empresaRepo; + _saldoRepo = saldoRepo; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private async Task 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> 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(); + foreach (var pago in pagos) + { + dtos.Add(await MapToDto(pago)); + } + return dtos; + } + + public async Task 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}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/ControlDevolucionesService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/ControlDevolucionesService.cs new file mode 100644 index 0000000..ec24942 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/ControlDevolucionesService.cs @@ -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 _logger; + + public ControlDevolucionesService( + IControlDevolucionesRepository controlDevRepo, + IEmpresaRepository empresaRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _controlDevRepo = controlDevRepo; + _empresaRepository = empresaRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private async Task 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> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idEmpresa) + { + var controles = await _controlDevRepo.GetAllAsync(fechaDesde, fechaHasta, idEmpresa); + var dtos = new List(); + foreach (var control in controles) + { + dtos.Add(await MapToDto(control)); + } + return dtos; + } + + public async Task 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}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/EntradaSalidaCanillaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/EntradaSalidaCanillaService.cs new file mode 100644 index 0000000..a3140df --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/EntradaSalidaCanillaService.cs @@ -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 _logger; + + public EntradaSalidaCanillaService( + IEntradaSalidaCanillaRepository esCanillaRepository, + IPublicacionRepository publicacionRepository, + ICanillaRepository canillaRepository, + IPrecioRepository precioRepository, + IRecargoZonaRepository recargoZonaRepository, + IPorcMonCanillaRepository porcMonCanillaRepository, + IUsuarioRepository usuarioRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _esCanillaRepository = esCanillaRepository; + _publicacionRepository = publicacionRepository; + _canillaRepository = canillaRepository; + _precioRepository = precioRepository; + _recargoZonaRepository = recargoZonaRepository; + _porcMonCanillaRepository = porcMonCanillaRepository; + _usuarioRepository = usuarioRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private async Task 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 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> 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(); + foreach (var mov in movimientos) + { + var dto = await MapToDto(mov); + if (dto != null) dtos.Add(dto); + } + return dtos; + } + + public async Task 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? 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 movimientosCreadosEntidades = new List(); + List erroresItems = new List(); + + 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(); + 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(); + } + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/EntradaSalidaDistService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/EntradaSalidaDistService.cs index 9b10f03..75f300d 100644 --- a/Backend/GestionIntegral.Api/Services/Distribucion/EntradaSalidaDistService.cs +++ b/Backend/GestionIntegral.Api/Services/Distribucion/EntradaSalidaDistService.cs @@ -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 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; } diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IControlDevolucionesService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IControlDevolucionesService.cs new file mode 100644 index 0000000..f2bfa5f --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IControlDevolucionesService.cs @@ -0,0 +1,16 @@ +using GestionIntegral.Api.Dtos.Distribucion; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IControlDevolucionesService + { + Task> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idEmpresa); + Task 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); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IEntradaSalidaCanillaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IEntradaSalidaCanillaService.cs new file mode 100644 index 0000000..3339197 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IEntradaSalidaCanillaService.cs @@ -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> ObtenerTodosAsync( + DateTime? fechaDesde, DateTime? fechaHasta, + int? idPublicacion, int? idCanilla, bool? liquidados, bool? incluirNoLiquidados); + + Task ObtenerPorIdAsync(int idParte); + Task<(IEnumerable? 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); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Radios/CancionService.cs b/Backend/GestionIntegral.Api/Services/Radios/CancionService.cs new file mode 100644 index 0000000..806693b --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Radios/CancionService.cs @@ -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 _logger; + + public CancionService( + ICancionRepository cancionRepository, + IRitmoRepository ritmoRepository, + ILogger logger) + { + _cancionRepository = cancionRepository; + _ritmoRepository = ritmoRepository; + _logger = logger; + } + + private async Task 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> ObtenerTodasAsync(string? temaFilter, string? interpreteFilter, int? idRitmoFilter) + { + var canciones = await _cancionRepository.GetAllAsync(temaFilter, interpreteFilter, idRitmoFilter); + var dtos = new List(); + foreach (var cancion in canciones) + { + dtos.Add(await MapToDto(cancion)); + } + return dtos; + } + + public async Task 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}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Radios/ICancionService.cs b/Backend/GestionIntegral.Api/Services/Radios/ICancionService.cs new file mode 100644 index 0000000..b9be884 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Radios/ICancionService.cs @@ -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> ObtenerTodasAsync(string? temaFilter, string? interpreteFilter, int? idRitmoFilter); + Task 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); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Radios/IRadioListaService.cs b/Backend/GestionIntegral.Api/Services/Radios/IRadioListaService.cs new file mode 100644 index 0000000..479e894 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Radios/IRadioListaService.cs @@ -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); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Radios/IRitmoService.cs b/Backend/GestionIntegral.Api/Services/Radios/IRitmoService.cs new file mode 100644 index 0000000..591a185 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Radios/IRitmoService.cs @@ -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> ObtenerTodosAsync(string? nombreFilter); + Task 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); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Radios/RadioListaService.cs b/Backend/GestionIntegral.Api/Services/Radios/RadioListaService.cs new file mode 100644 index 0000000..ac4fe4b --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Radios/RadioListaService.cs @@ -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 _logger; + + public RadioListaService( + ICancionRepository cancionRepository, + // IRitmoRepository ritmoRepository, // Puede ser removido si no se usa en otro lado + ILogger 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(); + 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(), "", "", "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(), "", "", $"Error interno al generar la lista: {ex.Message}"); + } + } + + private byte[] GenerarExcel(List 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(); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Radios/RitmoService.cs b/Backend/GestionIntegral.Api/Services/Radios/RitmoService.cs new file mode 100644 index 0000000..737f952 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Radios/RitmoService.cs @@ -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 _logger; + // private readonly DbConnectionFactory _connectionFactory; // Si se implementa historial + + public RitmoService(IRitmoRepository ritmoRepository, ILogger 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> ObtenerTodosAsync(string? nombreFilter) + { + var ritmos = await _ritmoRepository.GetAllAsync(nombreFilter); + return ritmos.Select(MapToDto); + } + + public async Task 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}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Usuarios/IUsuarioService.cs b/Backend/GestionIntegral.Api/Services/Usuarios/IUsuarioService.cs index da121ee..b8df9d8 100644 --- a/Backend/GestionIntegral.Api/Services/Usuarios/IUsuarioService.cs +++ b/Backend/GestionIntegral.Api/Services/Usuarios/IUsuarioService.cs @@ -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> ObtenerHistorialPorUsuarioIdAsync(int idUsuarioAfectado, DateTime? fechaDesde, DateTime? fechaHasta); + Task> ObtenerTodoElHistorialAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModificoFilter, string? tipoModFilter); } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Usuarios/UsuarioService.cs b/Backend/GestionIntegral.Api/Services/Usuarios/UsuarioService.cs index f9b0f38..974fc9c 100644 --- a/Backend/GestionIntegral.Api/Services/Usuarios/UsuarioService.cs +++ b/Backend/GestionIntegral.Api/Services/Usuarios/UsuarioService.cs @@ -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> 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> ObtenerTodoElHistorialAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idUsuarioModificoFilter, string? tipoModFilter) + { + // Aquí podrías añadir validaciones extra + return await _usuarioRepository.GetAllHistorialAsync(fechaDesde, fechaHasta, idUsuarioModificoFilter, tipoModFilter); + } } } \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/bin/Debug/net9.0/GestionIntegral.Api.deps.json b/Backend/GestionIntegral.Api/bin/Debug/net9.0/GestionIntegral.Api.deps.json index f9f0a7b..2f97328 100644 --- a/Backend/GestionIntegral.Api/bin/Debug/net9.0/GestionIntegral.Api.deps.json +++ b/Backend/GestionIntegral.Api/bin/Debug/net9.0/GestionIntegral.Api.deps.json @@ -12,6 +12,7 @@ "Microsoft.AspNetCore.Authentication.JwtBearer": "9.0.4", "Microsoft.AspNetCore.OpenApi": "9.0.3", "Microsoft.Data.SqlClient": "6.0.2", + "NPOI": "2.7.3", "Swashbuckle.AspNetCore": "8.1.1", "System.IdentityModel.Tokens.Jwt": "8.9.0" }, @@ -54,6 +55,14 @@ } } }, + "BouncyCastle.Cryptography/2.3.1": { + "runtime": { + "lib/net6.0/BouncyCastle.Cryptography.dll": { + "assemblyVersion": "2.0.0.0", + "fileVersion": "2.3.1.17862" + } + } + }, "Dapper/2.1.66": { "runtime": { "lib/net8.0/Dapper.dll": { @@ -62,6 +71,30 @@ } } }, + "Enums.NET/4.0.1": { + "runtime": { + "lib/netcoreapp3.0/Enums.NET.dll": { + "assemblyVersion": "4.0.0.0", + "fileVersion": "4.0.1.0" + } + } + }, + "ExtendedNumerics.BigDecimal/2025.1001.2.129": { + "runtime": { + "lib/net8.0/ExtendedNumerics.BigDecimal.dll": { + "assemblyVersion": "2025.1001.2.129", + "fileVersion": "2025.1001.2.129" + } + } + }, + "MathNet.Numerics.Signed/5.0.0": { + "runtime": { + "lib/net6.0/MathNet.Numerics.dll": { + "assemblyVersion": "5.0.0.0", + "fileVersion": "5.0.0.0" + } + } + }, "Microsoft.AspNetCore.Authentication.JwtBearer/9.0.4": { "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" @@ -348,6 +381,15 @@ } } }, + "Microsoft.IO.RecyclableMemoryStream/3.0.0": { + "runtime": { + "lib/net6.0/Microsoft.IO.RecyclableMemoryStream.dll": { + "assemblyVersion": "3.0.0.0", + "fileVersion": "3.0.0.0" + } + } + }, + "Microsoft.NETCore.Platforms/5.0.0": {}, "Microsoft.OpenApi/1.6.23": { "runtime": { "lib/netstandard2.0/Microsoft.OpenApi.dll": { @@ -364,6 +406,66 @@ } } }, + "NPOI/2.7.3": { + "dependencies": { + "BouncyCastle.Cryptography": "2.3.1", + "Enums.NET": "4.0.1", + "ExtendedNumerics.BigDecimal": "2025.1001.2.129", + "MathNet.Numerics.Signed": "5.0.0", + "Microsoft.IO.RecyclableMemoryStream": "3.0.0", + "SharpZipLib": "1.4.2", + "SixLabors.Fonts": "1.0.1", + "SixLabors.ImageSharp": "2.1.10", + "System.Security.Cryptography.Pkcs": "9.0.4", + "System.Security.Cryptography.Xml": "8.0.2" + }, + "runtime": { + "lib/net8.0/NPOI.Core.dll": { + "assemblyVersion": "2.7.3.0", + "fileVersion": "2.7.3.0" + }, + "lib/net8.0/NPOI.OOXML.dll": { + "assemblyVersion": "2.7.3.0", + "fileVersion": "2.7.3.0" + }, + "lib/net8.0/NPOI.OpenXml4Net.dll": { + "assemblyVersion": "2.7.3.0", + "fileVersion": "2.7.3.0" + }, + "lib/net8.0/NPOI.OpenXmlFormats.dll": { + "assemblyVersion": "2.7.3.0", + "fileVersion": "2.7.3.0" + } + } + }, + "SharpZipLib/1.4.2": { + "runtime": { + "lib/net6.0/ICSharpCode.SharpZipLib.dll": { + "assemblyVersion": "1.4.2.13", + "fileVersion": "1.4.2.13" + } + } + }, + "SixLabors.Fonts/1.0.1": { + "runtime": { + "lib/netcoreapp3.1/SixLabors.Fonts.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.1.0" + } + } + }, + "SixLabors.ImageSharp/2.1.10": { + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "5.0.0" + }, + "runtime": { + "lib/netcoreapp3.1/SixLabors.ImageSharp.dll": { + "assemblyVersion": "2.0.0.0", + "fileVersion": "2.1.10.0" + } + } + }, "Swashbuckle.AspNetCore/8.1.1": { "dependencies": { "Microsoft.Extensions.ApiDescription.Server": "6.0.5", @@ -498,6 +600,16 @@ } } }, + "System.Security.Cryptography.Xml/8.0.2": { + "dependencies": { + "System.Security.Cryptography.Pkcs": "9.0.4" + } + }, + "System.Text.Encoding.CodePages/5.0.0": { + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + } + }, "System.Text.Encodings.Web/4.7.2": {}, "System.Text.Json/4.7.2": {}, "System.Threading.Tasks.Extensions/4.5.4": {} @@ -523,6 +635,13 @@ "path": "azure.identity/1.11.4", "hashPath": "azure.identity.1.11.4.nupkg.sha512" }, + "BouncyCastle.Cryptography/2.3.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-buwoISwecYke3CmgG1AQSg+sNZjJeIb93vTAtJiHZX35hP/teYMxsfg0NDXGUKjGx6BKBTNKc77O2M3vKvlXZQ==", + "path": "bouncycastle.cryptography/2.3.1", + "hashPath": "bouncycastle.cryptography.2.3.1.nupkg.sha512" + }, "Dapper/2.1.66": { "type": "package", "serviceable": true, @@ -530,6 +649,27 @@ "path": "dapper/2.1.66", "hashPath": "dapper.2.1.66.nupkg.sha512" }, + "Enums.NET/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-OUGCd5L8zHZ61GAf436G0gf/H6yrSUkEpV5vm2CbCUuz9Rx7iLFLP5iHSSfmOtqNpuyo4vYte0CvYEmPZXRmRQ==", + "path": "enums.net/4.0.1", + "hashPath": "enums.net.4.0.1.nupkg.sha512" + }, + "ExtendedNumerics.BigDecimal/2025.1001.2.129": { + "type": "package", + "serviceable": true, + "sha512": "sha512-+woGT1lsBtwkntOpx2EZbdbySv0aWPefE0vrfvclxVdbi4oa2bbtphFPWgMiQe+kRCPICbfFJwp6w1DuR7Ge2Q==", + "path": "extendednumerics.bigdecimal/2025.1001.2.129", + "hashPath": "extendednumerics.bigdecimal.2025.1001.2.129.nupkg.sha512" + }, + "MathNet.Numerics.Signed/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-PSrHBVMf41SjbhlnpOMnoir8YgkyEJ6/nwxvjYpH+vJCexNcx2ms6zRww5yLVqLet1xLJgZ39swtKRTLhWdnAw==", + "path": "mathnet.numerics.signed/5.0.0", + "hashPath": "mathnet.numerics.signed.5.0.0.nupkg.sha512" + }, "Microsoft.AspNetCore.Authentication.JwtBearer/9.0.4": { "type": "package", "serviceable": true, @@ -677,6 +817,20 @@ "path": "microsoft.identitymodel.tokens/8.9.0", "hashPath": "microsoft.identitymodel.tokens.8.9.0.nupkg.sha512" }, + "Microsoft.IO.RecyclableMemoryStream/3.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-irv0HuqoH8Ig5i2fO+8dmDNdFdsrO+DoQcedwIlb810qpZHBNQHZLW7C/AHBQDgLLpw2T96vmMAy/aE4Yj55Sg==", + "path": "microsoft.io.recyclablememorystream/3.0.0", + "hashPath": "microsoft.io.recyclablememorystream.3.0.0.nupkg.sha512" + }, + "Microsoft.NETCore.Platforms/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==", + "path": "microsoft.netcore.platforms/5.0.0", + "hashPath": "microsoft.netcore.platforms.5.0.0.nupkg.sha512" + }, "Microsoft.OpenApi/1.6.23": { "type": "package", "serviceable": true, @@ -691,6 +845,34 @@ "path": "microsoft.sqlserver.server/1.0.0", "hashPath": "microsoft.sqlserver.server.1.0.0.nupkg.sha512" }, + "NPOI/2.7.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-iCZx3DSwUSwaV61E8tXgPlPuxYmcYV/Zi405nGlxQvWaGTAbuc0KvSBjsLucQUJ92iMeetT8iK9makLfF4uZ3g==", + "path": "npoi/2.7.3", + "hashPath": "npoi.2.7.3.nupkg.sha512" + }, + "SharpZipLib/1.4.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==", + "path": "sharpziplib/1.4.2", + "hashPath": "sharpziplib.1.4.2.nupkg.sha512" + }, + "SixLabors.Fonts/1.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ljezRHWc7N0azdQViib7Aa5v+DagRVkKI2/93kEbtjVczLs+yTkSq6gtGmvOcx4IqyNbO3GjLt7SAQTpLkySNw==", + "path": "sixlabors.fonts/1.0.1", + "hashPath": "sixlabors.fonts.1.0.1.nupkg.sha512" + }, + "SixLabors.ImageSharp/2.1.10": { + "type": "package", + "serviceable": true, + "sha512": "sha512-hk1E7U3RSlxrBVo6Gb6OjeM52fChpFYH+SZvyT/M2vzSGlzAaKE33hc3V/Pvnjcnn1opT8/Z+0QfqdM5HsIaeA==", + "path": "sixlabors.imagesharp/2.1.10", + "hashPath": "sixlabors.imagesharp.2.1.10.nupkg.sha512" + }, "Swashbuckle.AspNetCore/8.1.1": { "type": "package", "serviceable": true, @@ -796,6 +978,20 @@ "path": "system.security.cryptography.protecteddata/9.0.4", "hashPath": "system.security.cryptography.protecteddata.9.0.4.nupkg.sha512" }, + "System.Security.Cryptography.Xml/8.0.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-aDM/wm0ZGEZ6ZYJLzgqjp2FZdHbDHh6/OmpGfb7AdZ105zYmPn/83JRU2xLIbwgoNz9U1SLUTJN0v5th3qmvjA==", + "path": "system.security.cryptography.xml/8.0.2", + "hashPath": "system.security.cryptography.xml.8.0.2.nupkg.sha512" + }, + "System.Text.Encoding.CodePages/5.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "path": "system.text.encoding.codepages/5.0.0", + "hashPath": "system.text.encoding.codepages.5.0.0.nupkg.sha512" + }, "System.Text.Encodings.Web/4.7.2": { "type": "package", "serviceable": true, diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs index f9fcd33..ccd30e5 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+b6ba52f074807c7a2fddc76ab3cc2c45c446c1f8")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+e7e185a9cb2950cb77c96951d4c04ca768955d27")] [assembly: System.Reflection.AssemblyProductAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("GestionIntegral.Api")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.csproj.FileListAbsolute.txt b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.csproj.FileListAbsolute.txt index abac05b..3350a5f 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.csproj.FileListAbsolute.txt +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.csproj.FileListAbsolute.txt @@ -78,3 +78,15 @@ E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\GestionIntegr E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\GestionIntegral.Api.genruntimeconfig.cache E:\GestionIntegralWeb\backend\gestionintegral.api\obj\Debug\net9.0\ref\GestionIntegral.Api.dll E:\GestionIntegralWeb\Backend\GestionIntegral.Api\obj\Debug\net9.0\staticwebassets.build.json.cache +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\BouncyCastle.Cryptography.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\Enums.NET.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\ExtendedNumerics.BigDecimal.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\MathNet.Numerics.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\Microsoft.IO.RecyclableMemoryStream.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\NPOI.Core.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\NPOI.OOXML.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\NPOI.OpenXml4Net.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\NPOI.OpenXmlFormats.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\ICSharpCode.SharpZipLib.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\SixLabors.Fonts.dll +E:\GestionIntegralWeb\Backend\GestionIntegral.Api\bin\Debug\net9.0\SixLabors.ImageSharp.dll diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json index 0e9f146..56d834e 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","kULolnJcJq9du0a0dBwZaPVupTEFX15sai6mOONU2qk="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"C9goqBDGh4B0L1HpPwpJHjfbRNoIuzqnU7zFMHk1LhM=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","898tiJH7z5LyOA6iz/6l2n6u\u002Bf/3Afm3R6QKhRmMlf0="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json index dbe78cf..f9277b5 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/rjsmrazor.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","kULolnJcJq9du0a0dBwZaPVupTEFX15sai6mOONU2qk="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"w3MBbMV9Msh0YEq9AW/8s16bzXJ93T9lMVXKPm/r6es=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["lgiSIq1Xdt6PC6CpA82eiZlqBZS3M8jckHELlrL00LI=","bxlPVWHR7EivQofjz9PzA8dMpKpZqCfOZ\u002BHD\u002Bf1Ew9Y=","A4m4kVcox60bvdkJ1CswoZADAT70WPcs4TAKdpMoUjM=","zSzyOuNcK0NQJLwK8Yg4sH4EflX7RPf65Fl2CZUWIGs=","898tiJH7z5LyOA6iz/6l2n6u\u002Bf/3Afm3R6QKhRmMlf0="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.dgspec.json b/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.dgspec.json index d648775..3fed59d 100644 --- a/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.dgspec.json +++ b/Backend/GestionIntegral.Api/obj/GestionIntegral.Api.csproj.nuget.dgspec.json @@ -66,6 +66,10 @@ "target": "Package", "version": "[6.0.2, )" }, + "NPOI": { + "target": "Package", + "version": "[2.7.3, )" + }, "Swashbuckle.AspNetCore": { "target": "Package", "version": "[8.1.1, )" diff --git a/Backend/GestionIntegral.Api/obj/project.assets.json b/Backend/GestionIntegral.Api/obj/project.assets.json index f51c9f3..a6a613e 100644 --- a/Backend/GestionIntegral.Api/obj/project.assets.json +++ b/Backend/GestionIntegral.Api/obj/project.assets.json @@ -47,6 +47,19 @@ } } }, + "BouncyCastle.Cryptography/2.3.1": { + "type": "package", + "compile": { + "lib/net6.0/BouncyCastle.Cryptography.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/BouncyCastle.Cryptography.dll": { + "related": ".xml" + } + } + }, "Dapper/2.1.66": { "type": "package", "compile": { @@ -60,6 +73,45 @@ } } }, + "Enums.NET/4.0.1": { + "type": "package", + "compile": { + "lib/netcoreapp3.0/Enums.NET.dll": { + "related": ".pdb;.xml" + } + }, + "runtime": { + "lib/netcoreapp3.0/Enums.NET.dll": { + "related": ".pdb;.xml" + } + } + }, + "ExtendedNumerics.BigDecimal/2025.1001.2.129": { + "type": "package", + "compile": { + "lib/net8.0/ExtendedNumerics.BigDecimal.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net8.0/ExtendedNumerics.BigDecimal.dll": { + "related": ".xml" + } + } + }, + "MathNet.Numerics.Signed/5.0.0": { + "type": "package", + "compile": { + "lib/net6.0/MathNet.Numerics.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/MathNet.Numerics.dll": { + "related": ".xml" + } + } + }, "Microsoft.AspNetCore.Authentication.JwtBearer/9.0.4": { "type": "package", "dependencies": { @@ -470,6 +522,28 @@ } } }, + "Microsoft.IO.RecyclableMemoryStream/3.0.0": { + "type": "package", + "compile": { + "lib/net6.0/Microsoft.IO.RecyclableMemoryStream.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/Microsoft.IO.RecyclableMemoryStream.dll": { + "related": ".xml" + } + } + }, + "Microsoft.NETCore.Platforms/5.0.0": { + "type": "package", + "compile": { + "lib/netstandard1.0/_._": {} + }, + "runtime": { + "lib/netstandard1.0/_._": {} + } + }, "Microsoft.OpenApi/1.6.23": { "type": "package", "compile": { @@ -496,6 +570,92 @@ } } }, + "NPOI/2.7.3": { + "type": "package", + "dependencies": { + "BouncyCastle.Cryptography": "2.3.1", + "Enums.NET": "4.0.1", + "ExtendedNumerics.BigDecimal": "2025.1001.2.129", + "MathNet.Numerics.Signed": "5.0.0", + "Microsoft.IO.RecyclableMemoryStream": "3.0.0", + "SharpZipLib": "1.4.2", + "SixLabors.Fonts": "1.0.1", + "SixLabors.ImageSharp": "2.1.10", + "System.Security.Cryptography.Pkcs": "8.0.1", + "System.Security.Cryptography.Xml": "8.0.2" + }, + "compile": { + "lib/net8.0/NPOI.Core.dll": { + "related": ".pdb;.xml" + }, + "lib/net8.0/NPOI.OOXML.dll": { + "related": ".pdb;.xml" + }, + "lib/net8.0/NPOI.OpenXml4Net.dll": { + "related": ".pdb;.xml" + }, + "lib/net8.0/NPOI.OpenXmlFormats.dll": { + "related": ".pdb;.xml" + } + }, + "runtime": { + "lib/net8.0/NPOI.Core.dll": { + "related": ".pdb;.xml" + }, + "lib/net8.0/NPOI.OOXML.dll": { + "related": ".pdb;.xml" + }, + "lib/net8.0/NPOI.OpenXml4Net.dll": { + "related": ".pdb;.xml" + }, + "lib/net8.0/NPOI.OpenXmlFormats.dll": { + "related": ".pdb;.xml" + } + } + }, + "SharpZipLib/1.4.2": { + "type": "package", + "compile": { + "lib/net6.0/ICSharpCode.SharpZipLib.dll": { + "related": ".pdb;.xml" + } + }, + "runtime": { + "lib/net6.0/ICSharpCode.SharpZipLib.dll": { + "related": ".pdb;.xml" + } + } + }, + "SixLabors.Fonts/1.0.1": { + "type": "package", + "compile": { + "lib/netcoreapp3.1/SixLabors.Fonts.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netcoreapp3.1/SixLabors.Fonts.dll": { + "related": ".xml" + } + } + }, + "SixLabors.ImageSharp/2.1.10": { + "type": "package", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "5.0.0" + }, + "compile": { + "lib/netcoreapp3.1/SixLabors.ImageSharp.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netcoreapp3.1/SixLabors.ImageSharp.dll": { + "related": ".xml" + } + } + }, "Swashbuckle.AspNetCore/8.1.1": { "type": "package", "dependencies": { @@ -750,6 +910,47 @@ "buildTransitive/net8.0/_._": {} } }, + "System.Security.Cryptography.Xml/8.0.2": { + "type": "package", + "dependencies": { + "System.Security.Cryptography.Pkcs": "8.0.1" + }, + "compile": { + "lib/net8.0/System.Security.Cryptography.Xml.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net8.0/System.Security.Cryptography.Xml.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net6.0/_._": {} + } + }, + "System.Text.Encoding.CodePages/5.0.0": { + "type": "package", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + }, + "compile": { + "lib/netstandard2.0/System.Text.Encoding.CodePages.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/System.Text.Encoding.CodePages.dll": { + "related": ".xml" + } + }, + "runtimeTargets": { + "runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll": { + "assetType": "runtime", + "rid": "win" + } + } + }, "System.Text.Encodings.Web/4.7.2": { "type": "package", "compile": { @@ -826,6 +1027,26 @@ "lib/netstandard2.0/Azure.Identity.xml" ] }, + "BouncyCastle.Cryptography/2.3.1": { + "sha512": "buwoISwecYke3CmgG1AQSg+sNZjJeIb93vTAtJiHZX35hP/teYMxsfg0NDXGUKjGx6BKBTNKc77O2M3vKvlXZQ==", + "type": "package", + "path": "bouncycastle.cryptography/2.3.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.md", + "README.md", + "bouncycastle.cryptography.2.3.1.nupkg.sha512", + "bouncycastle.cryptography.nuspec", + "lib/net461/BouncyCastle.Cryptography.dll", + "lib/net461/BouncyCastle.Cryptography.xml", + "lib/net6.0/BouncyCastle.Cryptography.dll", + "lib/net6.0/BouncyCastle.Cryptography.xml", + "lib/netstandard2.0/BouncyCastle.Cryptography.dll", + "lib/netstandard2.0/BouncyCastle.Cryptography.xml", + "packageIcon.png" + ] + }, "Dapper/2.1.66": { "sha512": "/q77jUgDOS+bzkmk3Vy9SiWMaetTw+NOoPAV0xPBsGVAyljd5S6P+4RUW7R3ZUGGr9lDRyPKgAMj2UAOwvqZYw==", "type": "package", @@ -845,6 +1066,94 @@ "readme.md" ] }, + "Enums.NET/4.0.1": { + "sha512": "OUGCd5L8zHZ61GAf436G0gf/H6yrSUkEpV5vm2CbCUuz9Rx7iLFLP5iHSSfmOtqNpuyo4vYte0CvYEmPZXRmRQ==", + "type": "package", + "path": "enums.net/4.0.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "enums.net.4.0.1.nupkg.sha512", + "enums.net.nuspec", + "lib/net45/Enums.NET.dll", + "lib/net45/Enums.NET.pdb", + "lib/net45/Enums.NET.xml", + "lib/netcoreapp3.0/Enums.NET.dll", + "lib/netcoreapp3.0/Enums.NET.pdb", + "lib/netcoreapp3.0/Enums.NET.xml", + "lib/netstandard1.0/Enums.NET.dll", + "lib/netstandard1.0/Enums.NET.pdb", + "lib/netstandard1.0/Enums.NET.xml", + "lib/netstandard1.1/Enums.NET.dll", + "lib/netstandard1.1/Enums.NET.pdb", + "lib/netstandard1.1/Enums.NET.xml", + "lib/netstandard1.3/Enums.NET.dll", + "lib/netstandard1.3/Enums.NET.pdb", + "lib/netstandard1.3/Enums.NET.xml", + "lib/netstandard2.0/Enums.NET.dll", + "lib/netstandard2.0/Enums.NET.pdb", + "lib/netstandard2.0/Enums.NET.xml", + "lib/netstandard2.1/Enums.NET.dll", + "lib/netstandard2.1/Enums.NET.pdb", + "lib/netstandard2.1/Enums.NET.xml" + ] + }, + "ExtendedNumerics.BigDecimal/2025.1001.2.129": { + "sha512": "+woGT1lsBtwkntOpx2EZbdbySv0aWPefE0vrfvclxVdbi4oa2bbtphFPWgMiQe+kRCPICbfFJwp6w1DuR7Ge2Q==", + "type": "package", + "path": "extendednumerics.bigdecimal/2025.1001.2.129", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "extendednumerics.bigdecimal.2025.1001.2.129.nupkg.sha512", + "extendednumerics.bigdecimal.nuspec", + "lib/net45/ExtendedNumerics.BigDecimal.dll", + "lib/net45/ExtendedNumerics.BigDecimal.xml", + "lib/net46/ExtendedNumerics.BigDecimal.dll", + "lib/net46/ExtendedNumerics.BigDecimal.xml", + "lib/net472/ExtendedNumerics.BigDecimal.dll", + "lib/net472/ExtendedNumerics.BigDecimal.xml", + "lib/net48/ExtendedNumerics.BigDecimal.dll", + "lib/net48/ExtendedNumerics.BigDecimal.xml", + "lib/net5.0/ExtendedNumerics.BigDecimal.dll", + "lib/net5.0/ExtendedNumerics.BigDecimal.xml", + "lib/net6.0/ExtendedNumerics.BigDecimal.dll", + "lib/net6.0/ExtendedNumerics.BigDecimal.xml", + "lib/net7.0/ExtendedNumerics.BigDecimal.dll", + "lib/net7.0/ExtendedNumerics.BigDecimal.xml", + "lib/net8.0/ExtendedNumerics.BigDecimal.dll", + "lib/net8.0/ExtendedNumerics.BigDecimal.xml", + "lib/netcoreapp3.1/ExtendedNumerics.BigDecimal.dll", + "lib/netcoreapp3.1/ExtendedNumerics.BigDecimal.xml", + "lib/netstandard2.0/ExtendedNumerics.BigDecimal.dll", + "lib/netstandard2.0/ExtendedNumerics.BigDecimal.xml", + "lib/netstandard2.1/ExtendedNumerics.BigDecimal.dll", + "lib/netstandard2.1/ExtendedNumerics.BigDecimal.xml" + ] + }, + "MathNet.Numerics.Signed/5.0.0": { + "sha512": "PSrHBVMf41SjbhlnpOMnoir8YgkyEJ6/nwxvjYpH+vJCexNcx2ms6zRww5yLVqLet1xLJgZ39swtKRTLhWdnAw==", + "type": "package", + "path": "mathnet.numerics.signed/5.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "icon.png", + "lib/net461/MathNet.Numerics.dll", + "lib/net461/MathNet.Numerics.xml", + "lib/net48/MathNet.Numerics.dll", + "lib/net48/MathNet.Numerics.xml", + "lib/net5.0/MathNet.Numerics.dll", + "lib/net5.0/MathNet.Numerics.xml", + "lib/net6.0/MathNet.Numerics.dll", + "lib/net6.0/MathNet.Numerics.xml", + "lib/netstandard2.0/MathNet.Numerics.dll", + "lib/netstandard2.0/MathNet.Numerics.xml", + "mathnet.numerics.signed.5.0.0.nupkg.sha512", + "mathnet.numerics.signed.nuspec" + ] + }, "Microsoft.AspNetCore.Authentication.JwtBearer/9.0.4": { "sha512": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", "type": "package", @@ -1655,6 +1964,42 @@ "microsoft.identitymodel.tokens.nuspec" ] }, + "Microsoft.IO.RecyclableMemoryStream/3.0.0": { + "sha512": "irv0HuqoH8Ig5i2fO+8dmDNdFdsrO+DoQcedwIlb810qpZHBNQHZLW7C/AHBQDgLLpw2T96vmMAy/aE4Yj55Sg==", + "type": "package", + "path": "microsoft.io.recyclablememorystream/3.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "lib/net6.0/Microsoft.IO.RecyclableMemoryStream.dll", + "lib/net6.0/Microsoft.IO.RecyclableMemoryStream.xml", + "lib/netstandard2.0/Microsoft.IO.RecyclableMemoryStream.dll", + "lib/netstandard2.0/Microsoft.IO.RecyclableMemoryStream.xml", + "lib/netstandard2.1/Microsoft.IO.RecyclableMemoryStream.dll", + "lib/netstandard2.1/Microsoft.IO.RecyclableMemoryStream.xml", + "microsoft.io.recyclablememorystream.3.0.0.nupkg.sha512", + "microsoft.io.recyclablememorystream.nuspec" + ] + }, + "Microsoft.NETCore.Platforms/5.0.0": { + "sha512": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==", + "type": "package", + "path": "microsoft.netcore.platforms/5.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/netstandard1.0/_._", + "microsoft.netcore.platforms.5.0.0.nupkg.sha512", + "microsoft.netcore.platforms.nuspec", + "runtime.json", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, "Microsoft.OpenApi/1.6.23": { "sha512": "tZ1I0KXnn98CWuV8cpI247A17jaY+ILS9vvF7yhI0uPPEqF4P1d7BWL5Uwtel10w9NucllHB3nTkfYTAcHAh8g==", "type": "package", @@ -1688,6 +2033,132 @@ "microsoft.sqlserver.server.nuspec" ] }, + "NPOI/2.7.3": { + "sha512": "iCZx3DSwUSwaV61E8tXgPlPuxYmcYV/Zi405nGlxQvWaGTAbuc0KvSBjsLucQUJ92iMeetT8iK9makLfF4uZ3g==", + "type": "package", + "path": "npoi/2.7.3", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE", + "README.md", + "lib/net472/NPOI.Core.dll", + "lib/net472/NPOI.Core.pdb", + "lib/net472/NPOI.Core.xml", + "lib/net472/NPOI.OOXML.dll", + "lib/net472/NPOI.OOXML.pdb", + "lib/net472/NPOI.OOXML.xml", + "lib/net472/NPOI.OpenXml4Net.dll", + "lib/net472/NPOI.OpenXml4Net.pdb", + "lib/net472/NPOI.OpenXml4Net.xml", + "lib/net472/NPOI.OpenXmlFormats.dll", + "lib/net472/NPOI.OpenXmlFormats.pdb", + "lib/net472/NPOI.OpenXmlFormats.xml", + "lib/net8.0/NPOI.Core.dll", + "lib/net8.0/NPOI.Core.pdb", + "lib/net8.0/NPOI.Core.xml", + "lib/net8.0/NPOI.OOXML.dll", + "lib/net8.0/NPOI.OOXML.pdb", + "lib/net8.0/NPOI.OOXML.xml", + "lib/net8.0/NPOI.OpenXml4Net.dll", + "lib/net8.0/NPOI.OpenXml4Net.pdb", + "lib/net8.0/NPOI.OpenXml4Net.xml", + "lib/net8.0/NPOI.OpenXmlFormats.dll", + "lib/net8.0/NPOI.OpenXmlFormats.pdb", + "lib/net8.0/NPOI.OpenXmlFormats.xml", + "lib/netstandard2.0/NPOI.Core.dll", + "lib/netstandard2.0/NPOI.Core.pdb", + "lib/netstandard2.0/NPOI.Core.xml", + "lib/netstandard2.0/NPOI.OOXML.dll", + "lib/netstandard2.0/NPOI.OOXML.pdb", + "lib/netstandard2.0/NPOI.OOXML.xml", + "lib/netstandard2.0/NPOI.OpenXml4Net.dll", + "lib/netstandard2.0/NPOI.OpenXml4Net.pdb", + "lib/netstandard2.0/NPOI.OpenXml4Net.xml", + "lib/netstandard2.0/NPOI.OpenXmlFormats.dll", + "lib/netstandard2.0/NPOI.OpenXmlFormats.pdb", + "lib/netstandard2.0/NPOI.OpenXmlFormats.xml", + "lib/netstandard2.1/NPOI.Core.dll", + "lib/netstandard2.1/NPOI.Core.pdb", + "lib/netstandard2.1/NPOI.Core.xml", + "lib/netstandard2.1/NPOI.OOXML.dll", + "lib/netstandard2.1/NPOI.OOXML.pdb", + "lib/netstandard2.1/NPOI.OOXML.xml", + "lib/netstandard2.1/NPOI.OpenXml4Net.dll", + "lib/netstandard2.1/NPOI.OpenXml4Net.pdb", + "lib/netstandard2.1/NPOI.OpenXml4Net.xml", + "lib/netstandard2.1/NPOI.OpenXmlFormats.dll", + "lib/netstandard2.1/NPOI.OpenXmlFormats.pdb", + "lib/netstandard2.1/NPOI.OpenXmlFormats.xml", + "logo/120_120.jpg", + "logo/240_240.png", + "logo/32_32.jpg", + "logo/60_60.jpg", + "npoi.2.7.3.nupkg.sha512", + "npoi.nuspec" + ] + }, + "SharpZipLib/1.4.2": { + "sha512": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==", + "type": "package", + "path": "sharpziplib/1.4.2", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "images/sharpziplib-nuget-256x256.png", + "lib/net6.0/ICSharpCode.SharpZipLib.dll", + "lib/net6.0/ICSharpCode.SharpZipLib.pdb", + "lib/net6.0/ICSharpCode.SharpZipLib.xml", + "lib/netstandard2.0/ICSharpCode.SharpZipLib.dll", + "lib/netstandard2.0/ICSharpCode.SharpZipLib.pdb", + "lib/netstandard2.0/ICSharpCode.SharpZipLib.xml", + "lib/netstandard2.1/ICSharpCode.SharpZipLib.dll", + "lib/netstandard2.1/ICSharpCode.SharpZipLib.pdb", + "lib/netstandard2.1/ICSharpCode.SharpZipLib.xml", + "sharpziplib.1.4.2.nupkg.sha512", + "sharpziplib.nuspec" + ] + }, + "SixLabors.Fonts/1.0.1": { + "sha512": "ljezRHWc7N0azdQViib7Aa5v+DagRVkKI2/93kEbtjVczLs+yTkSq6gtGmvOcx4IqyNbO3GjLt7SAQTpLkySNw==", + "type": "package", + "path": "sixlabors.fonts/1.0.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/netcoreapp3.1/SixLabors.Fonts.dll", + "lib/netcoreapp3.1/SixLabors.Fonts.xml", + "lib/netstandard2.0/SixLabors.Fonts.dll", + "lib/netstandard2.0/SixLabors.Fonts.xml", + "lib/netstandard2.1/SixLabors.Fonts.dll", + "lib/netstandard2.1/SixLabors.Fonts.xml", + "sixlabors.fonts.1.0.1.nupkg.sha512", + "sixlabors.fonts.128.png", + "sixlabors.fonts.nuspec" + ] + }, + "SixLabors.ImageSharp/2.1.10": { + "sha512": "hk1E7U3RSlxrBVo6Gb6OjeM52fChpFYH+SZvyT/M2vzSGlzAaKE33hc3V/Pvnjcnn1opT8/Z+0QfqdM5HsIaeA==", + "type": "package", + "path": "sixlabors.imagesharp/2.1.10", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/net472/SixLabors.ImageSharp.dll", + "lib/net472/SixLabors.ImageSharp.xml", + "lib/netcoreapp2.1/SixLabors.ImageSharp.dll", + "lib/netcoreapp2.1/SixLabors.ImageSharp.xml", + "lib/netcoreapp3.1/SixLabors.ImageSharp.dll", + "lib/netcoreapp3.1/SixLabors.ImageSharp.xml", + "lib/netstandard2.0/SixLabors.ImageSharp.dll", + "lib/netstandard2.0/SixLabors.ImageSharp.xml", + "lib/netstandard2.1/SixLabors.ImageSharp.dll", + "lib/netstandard2.1/SixLabors.ImageSharp.xml", + "sixlabors.imagesharp.128.png", + "sixlabors.imagesharp.2.1.10.nupkg.sha512", + "sixlabors.imagesharp.nuspec" + ] + }, "Swashbuckle.AspNetCore/8.1.1": { "sha512": "HJHexmU0PiYevgTLvKjYkxEtclF2w4O7iTd3Ef3p6KeT0kcYLpkFVgCw6glpGS57h8769anv8G+NFi9Kge+/yw==", "type": "package", @@ -2074,6 +2545,76 @@ "useSharedDesignerContext.txt" ] }, + "System.Security.Cryptography.Xml/8.0.2": { + "sha512": "aDM/wm0ZGEZ6ZYJLzgqjp2FZdHbDHh6/OmpGfb7AdZ105zYmPn/83JRU2xLIbwgoNz9U1SLUTJN0v5th3qmvjA==", + "type": "package", + "path": "system.security.cryptography.xml/8.0.2", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/System.Security.Cryptography.Xml.targets", + "buildTransitive/net462/_._", + "buildTransitive/net6.0/_._", + "buildTransitive/netcoreapp2.0/System.Security.Cryptography.Xml.targets", + "lib/net462/System.Security.Cryptography.Xml.dll", + "lib/net462/System.Security.Cryptography.Xml.xml", + "lib/net6.0/System.Security.Cryptography.Xml.dll", + "lib/net6.0/System.Security.Cryptography.Xml.xml", + "lib/net7.0/System.Security.Cryptography.Xml.dll", + "lib/net7.0/System.Security.Cryptography.Xml.xml", + "lib/net8.0/System.Security.Cryptography.Xml.dll", + "lib/net8.0/System.Security.Cryptography.Xml.xml", + "lib/netstandard2.0/System.Security.Cryptography.Xml.dll", + "lib/netstandard2.0/System.Security.Cryptography.Xml.xml", + "system.security.cryptography.xml.8.0.2.nupkg.sha512", + "system.security.cryptography.xml.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "System.Text.Encoding.CodePages/5.0.0": { + "sha512": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "type": "package", + "path": "system.text.encoding.codepages/5.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net46/System.Text.Encoding.CodePages.dll", + "lib/net461/System.Text.Encoding.CodePages.dll", + "lib/net461/System.Text.Encoding.CodePages.xml", + "lib/netstandard1.3/System.Text.Encoding.CodePages.dll", + "lib/netstandard2.0/System.Text.Encoding.CodePages.dll", + "lib/netstandard2.0/System.Text.Encoding.CodePages.xml", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "lib/xamarintvos10/_._", + "lib/xamarinwatchos10/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._", + "ref/xamarintvos10/_._", + "ref/xamarinwatchos10/_._", + "runtimes/win/lib/net461/System.Text.Encoding.CodePages.dll", + "runtimes/win/lib/net461/System.Text.Encoding.CodePages.xml", + "runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.dll", + "runtimes/win/lib/netcoreapp2.0/System.Text.Encoding.CodePages.xml", + "runtimes/win/lib/netstandard1.3/System.Text.Encoding.CodePages.dll", + "runtimes/win/lib/netstandard2.0/System.Text.Encoding.CodePages.dll", + "runtimes/win/lib/netstandard2.0/System.Text.Encoding.CodePages.xml", + "system.text.encoding.codepages.5.0.0.nupkg.sha512", + "system.text.encoding.codepages.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, "System.Text.Encodings.Web/4.7.2": { "sha512": "iTUgB/WtrZ1sWZs84F2hwyQhiRH6QNjQv2DkwrH+WP6RoFga2Q1m3f9/Q7FG8cck8AdHitQkmkXSY8qylcDmuA==", "type": "package", @@ -2164,6 +2705,7 @@ "Microsoft.AspNetCore.Authentication.JwtBearer >= 9.0.4", "Microsoft.AspNetCore.OpenApi >= 9.0.3", "Microsoft.Data.SqlClient >= 6.0.2", + "NPOI >= 2.7.3", "Swashbuckle.AspNetCore >= 8.1.1", "System.IdentityModel.Tokens.Jwt >= 8.9.0" ] @@ -2234,6 +2776,10 @@ "target": "Package", "version": "[6.0.2, )" }, + "NPOI": { + "target": "Package", + "version": "[2.7.3, )" + }, "Swashbuckle.AspNetCore": { "target": "Package", "version": "[8.1.1, )" diff --git a/Frontend/src/components/Modals/Contables/NotaCreditoDebitoFormModal.tsx b/Frontend/src/components/Modals/Contables/NotaCreditoDebitoFormModal.tsx new file mode 100644 index 0000000..5ffb898 --- /dev/null +++ b/Frontend/src/components/Modals/Contables/NotaCreditoDebitoFormModal.tsx @@ -0,0 +1,271 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, RadioGroup, FormControlLabel, Radio, InputAdornment +} from '@mui/material'; +import type { NotaCreditoDebitoDto } from '../../../models/dtos/Contables/NotaCreditoDebitoDto'; +import type { CreateNotaDto } from '../../../models/dtos/Contables/CreateNotaDto'; +import type { UpdateNotaDto } from '../../../models/dtos/Contables/UpdateNotaDto'; +import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto'; +import type { DistribuidorDto } from '../../../models/dtos/Distribucion/DistribuidorDto'; +import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; // Para el dropdown +import empresaService from '../../../services/Distribucion/empresaService'; +import distribuidorService from '../../../services/Distribucion/distribuidorService'; +import canillaService from '../../../services/Distribucion/canillaService'; // Para cargar canillitas + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 550 }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +type DestinoType = 'Distribuidores' | 'Canillas'; +type TipoNotaType = 'Credito' | 'Debito'; + +interface NotaCreditoDebitoFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateNotaDto | UpdateNotaDto, idNota?: number) => Promise; + initialData?: NotaCreditoDebitoDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const NotaCreditoDebitoFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [destino, setDestino] = useState('Distribuidores'); + const [idDestino, setIdDestino] = useState(''); + const [referencia, setReferencia] = useState(''); + const [tipo, setTipo] = useState('Credito'); + const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]); + const [monto, setMonto] = useState(''); + const [observaciones, setObservaciones] = useState(''); + const [idEmpresa, setIdEmpresa] = useState(''); + + const [destinatarios, setDestinatarios] = useState<(DistribuidorDto | CanillaDto)[]>([]); + const [empresas, setEmpresas] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + const fetchEmpresas = useCallback(async () => { + setLoadingDropdowns(true); + try { + const data = await empresaService.getAllEmpresas(); + setEmpresas(data); + } catch (error) { + console.error("Error al cargar empresas", error); + setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar empresas.'})); + } finally { + setLoadingDropdowns(false); + } + }, []); + + const fetchDestinatarios = useCallback(async (tipoDestino: DestinoType) => { + setLoadingDropdowns(true); + setIdDestino(''); // Resetear selección de destinatario al cambiar tipo + setDestinatarios([]); + try { + if (tipoDestino === 'Distribuidores') { + const data = await distribuidorService.getAllDistribuidores(); + setDestinatarios(data); + } else if (tipoDestino === 'Canillas') { + const data = await canillaService.getAllCanillas(undefined, undefined, true); // Solo activos + setDestinatarios(data); + } + } catch (error) { + console.error(`Error al cargar ${tipoDestino}`, error); + setLocalErrors(prev => ({...prev, dropdowns: `Error al cargar ${tipoDestino}.`})); + } finally { + setLoadingDropdowns(false); + } + }, []); + + + useEffect(() => { + if (open) { + fetchEmpresas(); + // Cargar destinatarios basados en el initialData o el default + const initialDestinoType = initialData?.destino || 'Distribuidores'; + setDestino(initialDestinoType as DestinoType); + fetchDestinatarios(initialDestinoType as DestinoType); + + setIdDestino(initialData?.idDestino || ''); + setReferencia(initialData?.referencia || ''); + setTipo(initialData?.tipo as TipoNotaType || 'Credito'); + setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]); + setMonto(initialData?.monto?.toString() || ''); + setObservaciones(initialData?.observaciones || ''); + setIdEmpresa(initialData?.idEmpresa || ''); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage, fetchEmpresas, fetchDestinatarios]); + + useEffect(() => { + if(open && !isEditing) { // Solo cambiar destinatarios si es creación y cambia el tipo de Destino + fetchDestinatarios(destino); + } + }, [destino, open, isEditing, fetchDestinatarios]); + + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!destino) errors.destino = 'Seleccione un tipo de destino.'; + if (!idDestino) errors.idDestino = 'Seleccione un destinatario.'; + if (!tipo) errors.tipo = 'Seleccione el tipo de nota.'; + if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.'; + else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido.'; + if (!monto.trim() || isNaN(parseFloat(monto)) || parseFloat(monto) <= 0) errors.monto = 'Monto debe ser un número positivo.'; + if (!idEmpresa) errors.idEmpresa = 'Seleccione una empresa (para el saldo).'; + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + const montoNum = parseFloat(monto); + + if (isEditing && initialData) { + const dataToSubmit: UpdateNotaDto = { + monto: montoNum, + observaciones: observaciones || undefined, + }; + await onSubmit(dataToSubmit, initialData.idNota); + } else { + const dataToSubmit: CreateNotaDto = { + destino, + idDestino: Number(idDestino), + referencia: referencia || undefined, + tipo, + fecha, + monto: montoNum, + observaciones: observaciones || undefined, + idEmpresa: Number(idEmpresa), + }; + await onSubmit(dataToSubmit); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de NotaCreditoDebitoFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Nota de Crédito/Débito' : 'Registrar Nota de Crédito/Débito'} + + + + + Destino + {setDestino(e.target.value as DestinoType); handleInputChange('destino'); }}> + } label="Distribuidor" disabled={loading || isEditing}/> + } label="Canillita" disabled={loading || isEditing}/> + + + + + Destinatario + + {localErrors.idDestino && {localErrors.idDestino}} + + + + Empresa (del Saldo) + + {localErrors.idEmpresa && {localErrors.idEmpresa}} + + + {setFecha(e.target.value); handleInputChange('fecha');}} + margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} + disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing} + /> + setReferencia(e.target.value)} + margin="dense" fullWidth disabled={loading || isEditing} + /> + + Tipo de Nota + {setTipo(e.target.value as TipoNotaType); handleInputChange('tipo');}} > + } label="Crédito" disabled={loading || isEditing}/> + } label="Débito" disabled={loading || isEditing}/> + + {localErrors.tipo && {localErrors.tipo}} + + {setMonto(e.target.value); handleInputChange('monto');}} + margin="dense" fullWidth error={!!localErrors.monto} helperText={localErrors.monto || ''} + disabled={loading} inputProps={{step: "0.01", min:0.01, lang:"es-AR" }} + InputProps={{ startAdornment: $ }} + /> + setObservaciones(e.target.value)} + margin="dense" fullWidth multiline rows={2} disabled={loading} + /> + + + {errorMessage && {errorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + + + + ); +}; + +export default NotaCreditoDebitoFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Contables/PagoDistribuidorFormModal.tsx b/Frontend/src/components/Modals/Contables/PagoDistribuidorFormModal.tsx new file mode 100644 index 0000000..3c3da71 --- /dev/null +++ b/Frontend/src/components/Modals/Contables/PagoDistribuidorFormModal.tsx @@ -0,0 +1,248 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, RadioGroup, FormControlLabel, Radio, InputAdornment +} from '@mui/material'; +import type { PagoDistribuidorDto } from '../../../models/dtos/Contables/PagoDistribuidorDto'; +import type { CreatePagoDistribuidorDto } from '../../../models/dtos/Contables/CreatePagoDistribuidorDto'; +import type { UpdatePagoDistribuidorDto } from '../../../models/dtos/Contables/UpdatePagoDistribuidorDto'; +import type { DistribuidorDto } from '../../../models/dtos/Distribucion/DistribuidorDto'; +import type { TipoPago } from '../../../models/Entities/TipoPago'; +import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto'; +import distribuidorService from '../../../services/Distribucion/distribuidorService'; +import tipoPagoService from '../../../services/Contables/tipoPagoService'; +import empresaService from '../../../services/Distribucion/empresaService'; + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 600 }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface PagoDistribuidorFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreatePagoDistribuidorDto | UpdatePagoDistribuidorDto, idPago?: number) => Promise; + initialData?: PagoDistribuidorDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const PagoDistribuidorFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [idDistribuidor, setIdDistribuidor] = useState(''); + const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]); + const [tipoMovimiento, setTipoMovimiento] = useState<'Recibido' | 'Realizado'>('Recibido'); + const [recibo, setRecibo] = useState(''); + const [monto, setMonto] = useState(''); + const [idTipoPago, setIdTipoPago] = useState(''); + const [detalle, setDetalle] = useState(''); + const [idEmpresa, setIdEmpresa] = useState(''); + + + const [distribuidores, setDistribuidores] = useState([]); + const [tiposPago, setTiposPago] = useState([]); + const [empresas, setEmpresas] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchDropdownData = async () => { + setLoadingDropdowns(true); + try { + const [distData, tiposPagoData, empresasData] = await Promise.all([ + distribuidorService.getAllDistribuidores(), + tipoPagoService.getAllTiposPago(), + empresaService.getAllEmpresas() + ]); + setDistribuidores(distData); + setTiposPago(tiposPagoData); + setEmpresas(empresasData); + } catch (error) { + console.error("Error al cargar datos para dropdowns", error); + setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'})); + } finally { + setLoadingDropdowns(false); + } + }; + + if (open) { + fetchDropdownData(); + setIdDistribuidor(initialData?.idDistribuidor || ''); + setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]); + setTipoMovimiento(initialData?.tipoMovimiento || 'Recibido'); + setRecibo(initialData?.recibo?.toString() || ''); + setMonto(initialData?.monto?.toString() || ''); + setIdTipoPago(initialData?.idTipoPago || ''); + setDetalle(initialData?.detalle || ''); + setIdEmpresa(initialData?.idEmpresa || ''); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!idDistribuidor) errors.idDistribuidor = 'Seleccione un distribuidor.'; + if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.'; + else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido.'; + if (!tipoMovimiento) errors.tipoMovimiento = 'Seleccione un tipo de movimiento.'; + if (!recibo.trim() || isNaN(parseInt(recibo)) || parseInt(recibo) <= 0) errors.recibo = 'Nro. Recibo es obligatorio y numérico.'; + if (!monto.trim() || isNaN(parseFloat(monto)) || parseFloat(monto) <= 0) errors.monto = 'Monto debe ser un número positivo.'; + if (!idTipoPago) errors.idTipoPago = 'Seleccione un tipo de pago.'; + if (!idEmpresa) errors.idEmpresa = 'Seleccione una empresa (para el saldo).'; + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + const montoNum = parseFloat(monto); + + if (isEditing && initialData) { + const dataToSubmit: UpdatePagoDistribuidorDto = { + monto: montoNum, + idTipoPago: Number(idTipoPago), + detalle: detalle || undefined, + }; + await onSubmit(dataToSubmit, initialData.idPago); + } else { + const dataToSubmit: CreatePagoDistribuidorDto = { + idDistribuidor: Number(idDistribuidor), + fecha, + tipoMovimiento, + recibo: parseInt(recibo, 10), + monto: montoNum, + idTipoPago: Number(idTipoPago), + detalle: detalle || undefined, + idEmpresa: Number(idEmpresa), + }; + await onSubmit(dataToSubmit); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de PagoDistribuidorFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Pago de Distribuidor' : 'Registrar Nuevo Pago de Distribuidor'} + + + + + Distribuidor + + {localErrors.idDistribuidor && {localErrors.idDistribuidor}} + + + + Empresa (del Saldo) + + {localErrors.idEmpresa && {localErrors.idEmpresa}} + + + {setFecha(e.target.value); handleInputChange('fecha');}} + margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} + disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing} + /> + + + Tipo de Movimiento + {setTipoMovimiento(e.target.value as 'Recibido' | 'Realizado'); handleInputChange('tipoMovimiento');}} > + } label="Recibido de Distribuidor" disabled={loading || isEditing}/> + } label="Realizado a Distribuidor" disabled={loading || isEditing}/> + + {localErrors.tipoMovimiento && {localErrors.tipoMovimiento}} + + + {setRecibo(e.target.value); handleInputChange('recibo');}} + margin="dense" fullWidth error={!!localErrors.recibo} helperText={localErrors.recibo || ''} + disabled={loading || isEditing} inputProps={{min:1}} + /> + {setMonto(e.target.value); handleInputChange('monto');}} + margin="dense" fullWidth error={!!localErrors.monto} helperText={localErrors.monto || ''} + disabled={loading} inputProps={{step: "0.01", min:0.01, lang:"es-AR" }} + InputProps={{ startAdornment: $ }} + /> + + Tipo de Pago + + {localErrors.idTipoPago && {localErrors.idTipoPago}} + + setDetalle(e.target.value)} + margin="dense" fullWidth multiline rows={2} disabled={loading} + /> + + + {errorMessage && {errorMessage}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + + + + ); +}; + +export default PagoDistribuidorFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Contables/TipoPagoFormModal.tsx b/Frontend/src/components/Modals/Contables/TipoPagoFormModal.tsx index 537b13e..7e95a9a 100644 --- a/Frontend/src/components/Modals/Contables/TipoPagoFormModal.tsx +++ b/Frontend/src/components/Modals/Contables/TipoPagoFormModal.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; import type { TipoPago } from '../../../models/Entities/TipoPago'; -import type { CreateTipoPagoDto } from '../../../models/dtos/tiposPago/CreateTipoPagoDto'; +import type { CreateTipoPagoDto } from '../../../models/dtos/Contables/CreateTipoPagoDto'; const modalStyle = { position: 'absolute' as 'absolute', diff --git a/Frontend/src/components/Modals/Distribucion/ControlDevolucionesFormModal.tsx b/Frontend/src/components/Modals/Distribucion/ControlDevolucionesFormModal.tsx new file mode 100644 index 0000000..7604672 --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/ControlDevolucionesFormModal.tsx @@ -0,0 +1,201 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem +} from '@mui/material'; +import type { ControlDevolucionesDto } from '../../../models/dtos/Distribucion/ControlDevolucionesDto'; +import type { CreateControlDevolucionesDto } from '../../../models/dtos/Distribucion/CreateControlDevolucionesDto'; +import type { UpdateControlDevolucionesDto } from '../../../models/dtos/Distribucion/UpdateControlDevolucionesDto'; +import type { EmpresaDto } from '../../../models/dtos/Distribucion/EmpresaDto'; // DTO de Empresa +import empresaService from '../../../services//Distribucion/empresaService'; // Servicio de Empresa + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 500 }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface ControlDevolucionesFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateControlDevolucionesDto | UpdateControlDevolucionesDto, idControl?: number) => Promise; + initialData?: ControlDevolucionesDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const ControlDevolucionesFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [idEmpresa, setIdEmpresa] = useState(''); + const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]); + const [entrada, setEntrada] = useState(''); + const [sobrantes, setSobrantes] = useState(''); + const [detalle, setDetalle] = useState(''); + const [sinCargo, setSinCargo] = useState('0'); // Default 0 + + const [empresas, setEmpresas] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingEmpresas, setLoadingEmpresas] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchEmpresas = async () => { + setLoadingEmpresas(true); + try { + const data = await empresaService.getAllEmpresas(); + setEmpresas(data); + } catch (error) { + console.error("Error al cargar empresas", error); + setLocalErrors(prev => ({...prev, empresas: 'Error al cargar empresas.'})); + } finally { + setLoadingEmpresas(false); + } + }; + + if (open) { + fetchEmpresas(); + setIdEmpresa(initialData?.idEmpresa || ''); + setFecha(initialData?.fecha || new Date().toISOString().split('T')[0]); + setEntrada(initialData?.entrada?.toString() || '0'); + setSobrantes(initialData?.sobrantes?.toString() || '0'); + setDetalle(initialData?.detalle || ''); + setSinCargo(initialData?.sinCargo?.toString() || '0'); + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!idEmpresa) errors.idEmpresa = 'Seleccione una empresa.'; + if (!fecha.trim()) errors.fecha = 'La fecha es obligatoria.'; + else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) errors.fecha = 'Formato de fecha inválido.'; + + const entradaNum = parseInt(entrada, 10); + const sobrantesNum = parseInt(sobrantes, 10); + const sinCargoNum = parseInt(sinCargo, 10); + + if (entrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) errors.entrada = 'Entrada debe ser un número >= 0.'; + if (sobrantes.trim() === '' || isNaN(sobrantesNum) || sobrantesNum < 0) errors.sobrantes = 'Sobrantes debe ser un número >= 0.'; + if (sinCargo.trim() === '' || isNaN(sinCargoNum) || sinCargoNum < 0) errors.sinCargo = 'Sin Cargo debe ser un número >= 0.'; + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + const commonData = { + entrada: parseInt(entrada, 10), + sobrantes: parseInt(sobrantes, 10), + detalle: detalle || undefined, + sinCargo: parseInt(sinCargo, 10), + }; + + if (isEditing && initialData) { + const dataToSubmit: UpdateControlDevolucionesDto = { ...commonData }; + await onSubmit(dataToSubmit, initialData.idControl); + } else { + const dataToSubmit: CreateControlDevolucionesDto = { + ...commonData, + idEmpresa: Number(idEmpresa), + fecha, + }; + await onSubmit(dataToSubmit); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de ControlDevolucionesFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Control de Devoluciones' : 'Registrar Control de Devoluciones'} + + + + + Empresa + + {localErrors.idEmpresa && {localErrors.idEmpresa}} + + + {setFecha(e.target.value); handleInputChange('fecha');}} + margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} + disabled={loading || isEditing} InputLabelProps={{ shrink: true }} autoFocus={!isEditing} + /> + {setEntrada(e.target.value); handleInputChange('entrada');}} + margin="dense" fullWidth error={!!localErrors.entrada} helperText={localErrors.entrada || ''} + disabled={loading} inputProps={{min:0}} + /> + {setSobrantes(e.target.value); handleInputChange('sobrantes');}} + margin="dense" fullWidth error={!!localErrors.sobrantes} helperText={localErrors.sobrantes || ''} + disabled={loading} inputProps={{min:0}} + /> + {setSinCargo(e.target.value); handleInputChange('sinCargo');}} + margin="dense" fullWidth error={!!localErrors.sinCargo} helperText={localErrors.sinCargo || ''} + disabled={loading} inputProps={{min:0}} + /> + setDetalle(e.target.value)} + margin="dense" fullWidth multiline rows={2} disabled={loading} + /> + + + {errorMessage && {errorMessage}} + {localErrors.empresas && {localErrors.empresas}} + + + + + + + + + ); +}; + +export default ControlDevolucionesFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx b/Frontend/src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx new file mode 100644 index 0000000..3c344ca --- /dev/null +++ b/Frontend/src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx @@ -0,0 +1,396 @@ +// src/components/Modals/Distribucion/EntradaSalidaCanillaFormModal.tsx +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem, Paper, IconButton, FormHelperText +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; + +import type { EntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaDto'; +import type { UpdateEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto'; +import type { PublicacionDto } from '../../../models/dtos/Distribucion/PublicacionDto'; +import type { CanillaDto } from '../../../models/dtos/Distribucion/CanillaDto'; +import publicacionService from '../../../services/Distribucion/publicacionService'; +import canillaService from '../../../services/Distribucion/canillaService'; +import entradaSalidaCanillaService from '../../../services/Distribucion/entradaSalidaCanillaService'; +import type { CreateBulkEntradaSalidaCanillaDto } from '../../../models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto'; +import type { EntradaSalidaCanillaItemDto } from '../../../models/dtos/Distribucion/EntradaSalidaCanillaItemDto'; +import axios from 'axios'; + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '95%', sm: '80%', md: '750px' }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 2.5, + maxHeight: '90vh', + overflowY: 'auto' +}; + +interface EntradaSalidaCanillaFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: UpdateEntradaSalidaCanillaDto, idParte: number) => Promise; + initialData?: EntradaSalidaCanillaDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +interface FormRowItem { + id: string; + idPublicacion: number | string; + cantSalida: string; + cantEntrada: string; + observacion: string; +} + +const EntradaSalidaCanillaFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage: parentErrorMessage, + clearErrorMessage +}) => { + const [idCanilla, setIdCanilla] = useState(''); + const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]); + const [editIdPublicacion, setEditIdPublicacion] = useState(''); + const [editCantSalida, setEditCantSalida] = useState('0'); + const [editCantEntrada, setEditCantEntrada] = useState('0'); + const [editObservacion, setEditObservacion] = useState(''); + const [items, setItems] = useState([]); + const [publicaciones, setPublicaciones] = useState([]); + const [canillitas, setCanillitas] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + const [modalSpecificApiError, setModalSpecificApiError] = useState(null); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchDropdownData = async () => { + setLoadingDropdowns(true); + setLocalErrors(prev => ({ ...prev, dropdowns: null })); + try { + const [pubsData, canillitasData] = await Promise.all([ + publicacionService.getAllPublicaciones(undefined, undefined, true), + canillaService.getAllCanillas(undefined, undefined, true) + ]); + setPublicaciones(pubsData); + setCanillitas(canillitasData); + } catch (error) { + console.error("Error al cargar datos para dropdowns", error); + setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar datos necesarios (publicaciones/canillitas).' })); + } finally { + setLoadingDropdowns(false); + } + }; + + if (open) { + fetchDropdownData(); + clearErrorMessage(); + setModalSpecificApiError(null); + setLocalErrors({}); + + if (isEditing && initialData) { + setIdCanilla(initialData.idCanilla || ''); + setFecha(initialData.fecha ? initialData.fecha.split('T')[0] : new Date().toISOString().split('T')[0]); + setEditIdPublicacion(initialData.idPublicacion || ''); + setEditCantSalida(initialData.cantSalida?.toString() || '0'); + setEditCantEntrada(initialData.cantEntrada?.toString() || '0'); + setEditObservacion(initialData.observacion || ''); + setItems([]); + } else { + setItems([{ id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); + setIdCanilla(''); + setFecha(new Date().toISOString().split('T')[0]); + setEditCantSalida('0'); + setEditCantEntrada('0'); + setEditObservacion(''); + setEditIdPublicacion(''); + } + } + }, [open, initialData, isEditing, clearErrorMessage]); + + const validate = (): boolean => { + const currentErrors: { [key: string]: string | null } = {}; + if (!idCanilla) currentErrors.idCanilla = 'Seleccione un canillita.'; + if (!fecha.trim()) currentErrors.fecha = 'La fecha es obligatoria.'; + else if (!/^\d{4}-\d{2}-\d{2}$/.test(fecha)) currentErrors.fecha = 'Formato de fecha inválido (YYYY-MM-DD).'; + + if (isEditing) { + const salidaNum = parseInt(editCantSalida, 10); + const entradaNum = parseInt(editCantEntrada, 10); + if (editCantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) { + currentErrors.editCantSalida = 'Cant. Salida debe ser un número >= 0.'; + } + if (editCantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) { + currentErrors.editCantEntrada = 'Cant. Entrada debe ser un número >= 0.'; + } else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) { + currentErrors.editCantEntrada = 'Cant. Entrada no puede ser mayor a Cant. Salida.'; + } + } else { + let hasValidItemWithQuantityOrPub = false; + const publicacionIdsEnLote = new Set(); + + if (items.length === 0) { + currentErrors.general = "Debe agregar al menos una publicación."; + } + + items.forEach((item, index) => { + const salidaNum = parseInt(item.cantSalida, 10); + const entradaNum = parseInt(item.cantEntrada, 10); + const hasQuantity = !isNaN(salidaNum) && salidaNum >=0 && !isNaN(entradaNum) && entradaNum >=0 && (salidaNum > 0 || entradaNum > 0); + const hasObservation = item.observacion.trim() !== ''; + + if (item.idPublicacion === '') { + if (hasQuantity || hasObservation) { + currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} obligatoria si hay datos.`; + } + } else { + const pubIdNum = Number(item.idPublicacion); + if (publicacionIdsEnLote.has(pubIdNum)) { + currentErrors[`item_${item.id}_idPublicacion`] = `Pub. ${index + 1} duplicada.`; + } else { + publicacionIdsEnLote.add(pubIdNum); + } + if (item.cantSalida.trim() === '' || isNaN(salidaNum) || salidaNum < 0) { + currentErrors[`item_${item.id}_cantSalida`] = `Salida Pub. ${index + 1} inválida.`; + } + if (item.cantEntrada.trim() === '' || isNaN(entradaNum) || entradaNum < 0) { + currentErrors[`item_${item.id}_cantEntrada`] = `Entrada Pub. ${index + 1} inválida.`; + } else if (!isNaN(salidaNum) && !isNaN(entradaNum) && entradaNum > salidaNum) { + currentErrors[`item_${item.id}_cantEntrada`] = `Dev. Pub. ${index + 1} > Salida.`; + } + if (item.idPublicacion !== '') hasValidItemWithQuantityOrPub = true; + } + }); + + const allItemsAreEmptyAndNoPubSelected = items.every( + itm => itm.idPublicacion === '' && + (itm.cantSalida.trim() === '' || parseInt(itm.cantSalida, 10) === 0) && + (itm.cantEntrada.trim() === '' || parseInt(itm.cantEntrada, 10) === 0) && + itm.observacion.trim() === '' + ); + + if (!isEditing && items.length > 0 && !hasValidItemWithQuantityOrPub && !allItemsAreEmptyAndNoPubSelected) { + currentErrors.general = "Debe seleccionar una publicación para los ítems con cantidades y/o observaciones."; + } else if (!isEditing && items.length > 0 && !items.some(i => i.idPublicacion !== '' && (parseInt(i.cantSalida,10) > 0 || parseInt(i.cantEntrada,10) > 0)) && !allItemsAreEmptyAndNoPubSelected) { + currentErrors.general = "Debe ingresar cantidades para al menos una publicación con datos significativos."; + } + } + setLocalErrors(currentErrors); + return Object.keys(currentErrors).length === 0; + }; + + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + if (parentErrorMessage) clearErrorMessage(); + if (modalSpecificApiError) setModalSpecificApiError(null); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + setModalSpecificApiError(null); + if (!validate()) return; + + setLoading(true); + try { + if (isEditing && initialData) { + const salidaNum = parseInt(editCantSalida, 10); + const entradaNum = parseInt(editCantEntrada, 10); + const dataToSubmitSingle: UpdateEntradaSalidaCanillaDto = { + cantSalida: salidaNum, + cantEntrada: entradaNum, + observacion: editObservacion.trim() || undefined, + }; + await onSubmit(dataToSubmitSingle, initialData.idParte); + } else { + const itemsToSubmit: EntradaSalidaCanillaItemDto[] = items + .filter(item => + item.idPublicacion !== '' && + ( (parseInt(item.cantSalida, 10) >= 0 && parseInt(item.cantEntrada, 10) >= 0) && (parseInt(item.cantSalida,10) > 0 || parseInt(item.cantEntrada,10) > 0 ) || item.observacion.trim() !== '') + ) + .map(item => ({ + idPublicacion: Number(item.idPublicacion), + cantSalida: parseInt(item.cantSalida, 10) || 0, + cantEntrada: parseInt(item.cantEntrada, 10) || 0, + observacion: item.observacion.trim() || undefined, + })); + + if (itemsToSubmit.length === 0) { + setLocalErrors(prev => ({...prev, general: "No hay movimientos válidos para registrar. Asegúrese de seleccionar una publicación y/o ingresar cantidades."})); + setLoading(false); + return; + } + + const bulkData: CreateBulkEntradaSalidaCanillaDto = { + idCanilla: Number(idCanilla), + fecha, + items: itemsToSubmit, + }; + await entradaSalidaCanillaService.createBulkEntradasSalidasCanilla(bulkData); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de EntradaSalidaCanillaFormModal:", error); + if (axios.isAxiosError(error) && error.response) { + setModalSpecificApiError(error.response.data?.message || 'Error al procesar la solicitud.'); + } else { + setModalSpecificApiError('Ocurrió un error inesperado.'); + } + if (isEditing) throw error; + } finally { + setLoading(false); + } + }; + + const handleAddRow = () => { + if (items.length >= publicaciones.length) { + setLocalErrors(prev => ({ ...prev, general: "No se pueden agregar más filas que publicaciones disponibles." })); + return; + } + setItems([...items, { id: Date.now().toString(), idPublicacion: '', cantSalida: '0', cantEntrada: '0', observacion: '' }]); + setLocalErrors(prev => ({ ...prev, general: null })); + }; + + const handleRemoveRow = (idToRemove: string) => { + if (items.length <= 1 && !isEditing) return; + setItems(items.filter(item => item.id !== idToRemove)); + }; + + const handleItemChange = (id: string, field: keyof Omit, value: string | number) => { + setItems(items.map(itemRow => itemRow.id === id ? { ...itemRow, [field]: value } : itemRow)); // CORREGIDO: item a itemRow para evitar conflicto de nombres de variable con el `item` del map en el JSX + if (localErrors[`item_${id}_${field}`]) { // Aquí item se refiere al id del item. + setLocalErrors(prev => ({ ...prev, [`item_${id}_${field}`]: null })); + } + if (localErrors.general) setLocalErrors(prev => ({ ...prev, general: null })); + if (parentErrorMessage) clearErrorMessage(); + if (modalSpecificApiError) setModalSpecificApiError(null); + }; + + return ( + + + + {isEditing ? 'Editar Movimiento Canillita' : 'Registrar Movimientos Canillita'} + + + + + Canillita + + {localErrors.idCanilla && {localErrors.idCanilla}} + + + { setFecha(e.target.value); handleInputChange('fecha'); }} + margin="dense" fullWidth error={!!localErrors.fecha} helperText={localErrors.fecha || ''} + disabled={loading || isEditing} InputLabelProps={{ shrink: true }} + autoFocus={!isEditing} + /> + + {isEditing && initialData && ( + + Editando para Publicación: {publicaciones.find(p=>p.idPublicacion === editIdPublicacion)?.nombre || `ID ${editIdPublicacion}`} + + {setEditCantSalida(e.target.value); handleInputChange('editCantSalida');}} + margin="dense" fullWidth error={!!localErrors.editCantSalida} helperText={localErrors.editCantSalida || ''} + disabled={loading} inputProps={{ min: 0 }} sx={{flex:1}} + /> + {setEditCantEntrada(e.target.value); handleInputChange('editCantEntrada');}} + margin="dense" fullWidth error={!!localErrors.editCantEntrada} helperText={localErrors.editCantEntrada || ''} + disabled={loading} inputProps={{ min: 0 }} sx={{flex:1}} + /> + + setEditObservacion(e.target.value)} + margin="dense" fullWidth multiline rows={2} disabled={loading} sx={{mt:1}} + /> + + )} + + {!isEditing && ( + + Movimientos por Publicación: + {items.map((itemRow, index) => ( // item renombrado a itemRow + + {items.length > 1 && ( + handleRemoveRow(itemRow.id)} color="error" size="small" + sx={{ position: 'absolute', top: 4, right: 4, zIndex:1 }} + aria-label="Quitar fila" + > + + + )} + + + 0 || parseInt(itemRow.cantEntrada) > 0 || itemRow.observacion.trim() !== ''}>Pub. {index + 1} + + {localErrors[`item_${itemRow.id}_idPublicacion`] && {localErrors[`item_${itemRow.id}_idPublicacion`]}} + + handleItemChange(itemRow.id, 'cantSalida', e.target.value)} + error={!!localErrors[`item_${itemRow.id}_cantSalida`]} helperText={localErrors[`item_${itemRow.id}_cantSalida`]} + inputProps={{ min: 0 }} sx={{ width: 'auto', flexBasis: 'calc(15% - 8px)', minWidth: '80px' }} + /> + handleItemChange(itemRow.id, 'cantEntrada', e.target.value)} + error={!!localErrors[`item_${itemRow.id}_cantEntrada`]} helperText={localErrors[`item_${itemRow.id}_cantEntrada`]} + inputProps={{ min: 0 }} sx={{ width: 'auto', flexBasis: 'calc(15% - 8px)', minWidth: '80px' }} + /> + handleItemChange(itemRow.id, 'observacion', e.target.value)} + size="small" sx={{ flexGrow: 1, flexBasis: 'calc(25% - 8px)', minWidth: '120px' }} multiline maxRows={1} + /> + + + ))} + {localErrors.general && {localErrors.general}} + + + )} + + + {parentErrorMessage && {parentErrorMessage}} + {modalSpecificApiError && {modalSpecificApiError}} + {localErrors.dropdowns && {localErrors.dropdowns}} + + + + + + + + + ); +}; + +export default EntradaSalidaCanillaFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Distribucion/EntradaSalidaDistFormModal.tsx b/Frontend/src/components/Modals/Distribucion/EntradaSalidaDistFormModal.tsx index 89c0894..5b7e644 100644 --- a/Frontend/src/components/Modals/Distribucion/EntradaSalidaDistFormModal.tsx +++ b/Frontend/src/components/Modals/Distribucion/EntradaSalidaDistFormModal.tsx @@ -29,7 +29,7 @@ interface EntradaSalidaDistFormModalProps { open: boolean; onClose: () => void; onSubmit: (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => Promise; - initialData?: EntradaSalidaDistDto | null; // Para editar + initialData?: EntradaSalidaDistDto | null; errorMessage?: string | null; clearErrorMessage: () => void; } @@ -63,8 +63,8 @@ const EntradaSalidaDistFormModal: React.FC = ({ setLoadingDropdowns(true); try { const [pubsData, distData] = await Promise.all([ - publicacionService.getAllPublicaciones(undefined, undefined, true), // Solo habilitadas - distribuidorService.getAllDistribuidores() + publicacionService.getAllPublicaciones(undefined, undefined, true), + distribuidorService.getAllDistribuidores() // Asume que este servicio existe y funciona ]); setPublicaciones(pubsData); setDistribuidores(distData); @@ -100,7 +100,7 @@ const EntradaSalidaDistFormModal: React.FC = ({ if (!cantidad.trim() || isNaN(parseInt(cantidad)) || parseInt(cantidad) <= 0) { errors.cantidad = 'La cantidad debe ser un número positivo.'; } - if (!isEditing && (!remito.trim() || isNaN(parseInt(remito)) || parseInt(remito) <= 0)) { + if (!remito.trim() || isNaN(parseInt(remito)) || parseInt(remito) <= 0) { // Remito obligatorio en creación y edición errors.remito = 'El Nro. Remito es obligatorio y debe ser un número positivo.'; } setLocalErrors(errors); @@ -123,6 +123,7 @@ const EntradaSalidaDistFormModal: React.FC = ({ const dataToSubmit: UpdateEntradaSalidaDistDto = { cantidad: parseInt(cantidad, 10), observacion: observacion || undefined, + // Remito no se edita según el DTO de Update }; await onSubmit(dataToSubmit, initialData.idParte); } else { @@ -184,15 +185,15 @@ const EntradaSalidaDistFormModal: React.FC = ({ /> - Tipo de Movimiento + Tipo Movimiento {setTipoMovimiento(e.target.value as 'Salida' | 'Entrada'); handleInputChange('tipoMovimiento');}} > - } label="Salida (a Distribuidor)" disabled={loading || isEditing}/> - } label="Entrada (de Distribuidor)" disabled={loading || isEditing}/> + } label="Salida" disabled={loading || isEditing}/> + } label="Entrada" disabled={loading || isEditing}/> {localErrors.tipoMovimiento && {localErrors.tipoMovimiento}} - {setRemito(e.target.value); handleInputChange('remito');}} margin="dense" fullWidth error={!!localErrors.remito} helperText={localErrors.remito || ''} disabled={loading || isEditing} inputProps={{min:1}} diff --git a/Frontend/src/components/Modals/Radios/CancionFormModal.tsx b/Frontend/src/components/Modals/Radios/CancionFormModal.tsx new file mode 100644 index 0000000..142a31c --- /dev/null +++ b/Frontend/src/components/Modals/Radios/CancionFormModal.tsx @@ -0,0 +1,212 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert, + FormControl, InputLabel, Select, MenuItem +} from '@mui/material'; +import type { CancionDto } from '../../../models/dtos/Radios/CancionDto'; +import type { CreateCancionDto } from '../../../models/dtos/Radios/CreateCancionDto'; +import type { UpdateCancionDto } from '../../../models/dtos/Radios/UpdateCancionDto'; +import type { RitmoDto } from '../../../models/dtos/Radios/RitmoDto'; // Para el dropdown de ritmos +import ritmoService from '../../../services/Radios/ritmoService'; // Para cargar ritmos + +const modalStyle = { /* ... (mismo estilo, pero más ancho y alto) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '95%', sm: '80%', md: '700px' }, // Más ancho + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 3, + maxHeight: '90vh', // Permitir scroll + overflowY: 'auto' +}; + +interface CancionFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateCancionDto | UpdateCancionDto, id?: number) => Promise; + initialData?: CancionDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +type CancionFormState = Omit & { + pista: string; // TextField siempre es string + idRitmo: number | string; // Select puede ser string vacío +}; + + +const CancionFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const initialFormState: CancionFormState = { + tema: '', compositorAutor: '', interprete: '', sello: '', placa: '', + pista: '', introduccion: '', idRitmo: '', formato: '', album: '' + }; + const [formState, setFormState] = useState(initialFormState); + const [ritmos, setRitmos] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingRitmos, setLoadingRitmos] = useState(false); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const isEditing = Boolean(initialData); + + useEffect(() => { + const fetchRitmos = async () => { + setLoadingRitmos(true); + try { + const data = await ritmoService.getAllRitmos(); + setRitmos(data); + } catch (error) { + console.error("Error al cargar ritmos", error); + setLocalErrors(prev => ({...prev, ritmos: 'Error al cargar ritmos.'})); + } finally { + setLoadingRitmos(false); + } + }; + + if (open) { + fetchRitmos(); + if (initialData) { + setFormState({ + tema: initialData.tema || '', + compositorAutor: initialData.compositorAutor || '', + interprete: initialData.interprete || '', + sello: initialData.sello || '', + placa: initialData.placa || '', + pista: initialData.pista?.toString() || '', + introduccion: initialData.introduccion || '', + idRitmo: initialData.idRitmo || '', + formato: initialData.formato || '', + album: initialData.album || '', + }); + } else { + setFormState(initialFormState); + } + setLocalErrors({}); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); // No incluir initialFormState aquí directamente + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + if (!formState.tema?.trim() && !formState.interprete?.trim()) { // Al menos tema o intérprete + errors.tema = 'Se requiere al menos el Tema o el Intérprete.'; + errors.interprete = 'Se requiere al menos el Tema o el Intérprete.'; + } + if (formState.pista.trim() && isNaN(parseInt(formState.pista))) { + errors.pista = 'Pista debe ser un número.'; + } + // Otras validaciones específicas si son necesarias + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleChange = (event: React.ChangeEvent | (Event & { target: { name: string; value: unknown } })) => { + const { name, value } = event.target as { name: keyof CancionFormState, value: string }; + setFormState(prev => ({ ...prev, [name]: value })); + if (localErrors[name]) { + setLocalErrors(prev => ({ ...prev, [name]: null })); + } + if (errorMessage) clearErrorMessage(); + }; + + const handleSelectChange = (event: React.ChangeEvent<{ name?: string; value: unknown }>) => { + const name = event.target.name as keyof CancionFormState; + setFormState(prev => ({ ...prev, [name]: event.target.value as string | number })); + if (localErrors[name]) { + setLocalErrors(prev => ({ ...prev, [name]: null })); + } + if (errorMessage) clearErrorMessage(); + }; + + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + const dataToSubmit: CreateCancionDto | UpdateCancionDto = { + ...formState, + pista: formState.pista.trim() ? parseInt(formState.pista, 10) : null, + idRitmo: formState.idRitmo ? Number(formState.idRitmo) : null, + // Convertir strings vacíos a undefined para que no se envíen si son opcionales en el backend + tema: formState.tema?.trim() || undefined, + compositorAutor: formState.compositorAutor?.trim() || undefined, + interprete: formState.interprete?.trim() || undefined, + sello: formState.sello?.trim() || undefined, + placa: formState.placa?.trim() || undefined, + introduccion: formState.introduccion?.trim() || undefined, + formato: formState.formato?.trim() || undefined, + album: formState.album?.trim() || undefined, + }; + + if (isEditing && initialData) { + await onSubmit(dataToSubmit as UpdateCancionDto, initialData.id); + } else { + await onSubmit(dataToSubmit as CreateCancionDto); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de CancionFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Canción' : 'Agregar Nueva Canción'} + + + + + + + + + + + + + + + Ritmo + + + + + + + + + {errorMessage && {errorMessage}} + {localErrors.ritmos && {localErrors.ritmos}} + + + + + + + + + ); +}; + +export default CancionFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/Radios/RitmoFormModal.tsx b/Frontend/src/components/Modals/Radios/RitmoFormModal.tsx new file mode 100644 index 0000000..0f10ad3 --- /dev/null +++ b/Frontend/src/components/Modals/Radios/RitmoFormModal.tsx @@ -0,0 +1,111 @@ +import React, { useState, useEffect } from 'react'; +import { + Modal, Box, Typography, TextField, Button, CircularProgress, Alert +} from '@mui/material'; +import type { RitmoDto } from '../../../models/dtos/Radios/RitmoDto'; +import type { CreateRitmoDto } from '../../../models/dtos/Radios/CreateRitmoDto'; +import type { UpdateRitmoDto } from '../../../models/dtos/Radios/UpdateRitmoDto'; + +const modalStyle = { /* ... (mismo estilo) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: { xs: '90%', sm: 400 }, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +interface RitmoFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateRitmoDto | UpdateRitmoDto, id?: number) => Promise; + initialData?: RitmoDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const RitmoFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [nombreRitmo, setNombreRitmo] = useState(''); + const [loading, setLoading] = useState(false); + const [localErrorNombre, setLocalErrorNombre] = useState(null); + + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + setNombreRitmo(initialData?.nombreRitmo || ''); + setLocalErrorNombre(null); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const validate = (): boolean => { + if (!nombreRitmo.trim()) { + setLocalErrorNombre('El nombre del ritmo es obligatorio.'); + return false; + } + return true; + }; + + const handleInputChange = () => { + if (localErrorNombre) setLocalErrorNombre(null); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + clearErrorMessage(); + if (!validate()) return; + + setLoading(true); + try { + const dataToSubmit = { nombreRitmo }; + if (isEditing && initialData) { + await onSubmit(dataToSubmit as UpdateRitmoDto, initialData.id); + } else { + await onSubmit(dataToSubmit as CreateRitmoDto); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de RitmoFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Ritmo' : 'Agregar Nuevo Ritmo'} + + + {setNombreRitmo(e.target.value); handleInputChange();}} + margin="dense" fullWidth error={!!localErrorNombre} helperText={localErrorNombre || ''} + disabled={loading} autoFocus + /> + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default RitmoFormModal; \ No newline at end of file diff --git a/Frontend/src/models/Empresa.ts b/Frontend/src/models/Entities/Empresa.ts similarity index 100% rename from Frontend/src/models/Empresa.ts rename to Frontend/src/models/Entities/Empresa.ts diff --git a/Frontend/src/models/dtos/Contables/CreateNotaDto.ts b/Frontend/src/models/dtos/Contables/CreateNotaDto.ts new file mode 100644 index 0000000..3e32371 --- /dev/null +++ b/Frontend/src/models/dtos/Contables/CreateNotaDto.ts @@ -0,0 +1,10 @@ +export interface CreateNotaDto { + destino: 'Distribuidores' | 'Canillas'; + idDestino: number; + referencia?: string | null; + tipo: 'Debito' | 'Credito'; + fecha: string; // "yyyy-MM-dd" + monto: number; + observaciones?: string | null; + idEmpresa: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Contables/CreatePagoDistribuidorDto.ts b/Frontend/src/models/dtos/Contables/CreatePagoDistribuidorDto.ts new file mode 100644 index 0000000..1536255 --- /dev/null +++ b/Frontend/src/models/dtos/Contables/CreatePagoDistribuidorDto.ts @@ -0,0 +1,10 @@ +export interface CreatePagoDistribuidorDto { + idDistribuidor: number; + fecha: string; // "yyyy-MM-dd" + tipoMovimiento: 'Recibido' | 'Realizado'; + recibo: number; + monto: number; + idTipoPago: number; + detalle?: string | null; + idEmpresa: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/tiposPago/CreateTipoPagoDto.ts b/Frontend/src/models/dtos/Contables/CreateTipoPagoDto.ts similarity index 100% rename from Frontend/src/models/dtos/tiposPago/CreateTipoPagoDto.ts rename to Frontend/src/models/dtos/Contables/CreateTipoPagoDto.ts diff --git a/Frontend/src/models/dtos/Contables/NotaCreditoDebitoDto.ts b/Frontend/src/models/dtos/Contables/NotaCreditoDebitoDto.ts new file mode 100644 index 0000000..06c1bac --- /dev/null +++ b/Frontend/src/models/dtos/Contables/NotaCreditoDebitoDto.ts @@ -0,0 +1,13 @@ +export interface NotaCreditoDebitoDto { + idNota: number; + destino: 'Distribuidores' | 'Canillas'; + idDestino: number; + nombreDestinatario: string; + referencia?: string | null; + tipo: 'Debito' | 'Credito'; + fecha: string; // "yyyy-MM-dd" + monto: number; + observaciones?: string | null; + idEmpresa: number; + nombreEmpresa: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Contables/PagoDistribuidorDto.ts b/Frontend/src/models/dtos/Contables/PagoDistribuidorDto.ts new file mode 100644 index 0000000..19602a5 --- /dev/null +++ b/Frontend/src/models/dtos/Contables/PagoDistribuidorDto.ts @@ -0,0 +1,14 @@ +export interface PagoDistribuidorDto { + idPago: number; + idDistribuidor: number; + nombreDistribuidor: string; + fecha: string; // "yyyy-MM-dd" + tipoMovimiento: 'Recibido' | 'Realizado'; + recibo: number; + monto: number; + idTipoPago: number; + nombreTipoPago: string; + detalle?: string | null; + idEmpresa: number; + nombreEmpresa: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Contables/UpdateNotaDto.ts b/Frontend/src/models/dtos/Contables/UpdateNotaDto.ts new file mode 100644 index 0000000..bb5e083 --- /dev/null +++ b/Frontend/src/models/dtos/Contables/UpdateNotaDto.ts @@ -0,0 +1,4 @@ +export interface UpdateNotaDto { + monto: number; + observaciones?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Contables/UpdatePagoDistribuidorDto.ts b/Frontend/src/models/dtos/Contables/UpdatePagoDistribuidorDto.ts new file mode 100644 index 0000000..a9fe3b8 --- /dev/null +++ b/Frontend/src/models/dtos/Contables/UpdatePagoDistribuidorDto.ts @@ -0,0 +1,5 @@ +export interface UpdatePagoDistribuidorDto { + monto: number; + idTipoPago: number; + detalle?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/tiposPago/UpdateTipoPagoDto.ts b/Frontend/src/models/dtos/Contables/UpdateTipoPagoDto.ts similarity index 100% rename from Frontend/src/models/dtos/tiposPago/UpdateTipoPagoDto.ts rename to Frontend/src/models/dtos/Contables/UpdateTipoPagoDto.ts diff --git a/Frontend/src/models/dtos/Distribucion/ControlDevolucionesDto.ts b/Frontend/src/models/dtos/Distribucion/ControlDevolucionesDto.ts new file mode 100644 index 0000000..d95b7b5 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/ControlDevolucionesDto.ts @@ -0,0 +1,10 @@ +export interface ControlDevolucionesDto { + idControl: number; + idEmpresa: number; + nombreEmpresa: string; + fecha: string; // "yyyy-MM-dd" + entrada: number; + sobrantes: number; + detalle?: string | null; + sinCargo: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto.ts new file mode 100644 index 0000000..164d951 --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto.ts @@ -0,0 +1,7 @@ +import type { EntradaSalidaCanillaItemDto } from './EntradaSalidaCanillaItemDto'; + +export interface CreateBulkEntradaSalidaCanillaDto { + idCanilla: number; + fecha: string; // "yyyy-MM-dd" + items: EntradaSalidaCanillaItemDto[]; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreateControlDevolucionesDto.ts b/Frontend/src/models/dtos/Distribucion/CreateControlDevolucionesDto.ts new file mode 100644 index 0000000..2324c8a --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreateControlDevolucionesDto.ts @@ -0,0 +1,8 @@ +export interface CreateControlDevolucionesDto { + idEmpresa: number; + fecha: string; // "yyyy-MM-dd" + entrada: number; + sobrantes: number; + detalle?: string | null; + sinCargo: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/CreateEntradaSalidaCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/CreateEntradaSalidaCanillaDto.ts new file mode 100644 index 0000000..bfbe77d --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/CreateEntradaSalidaCanillaDto.ts @@ -0,0 +1,8 @@ +export interface CreateEntradaSalidaCanillaDto { + idPublicacion: number; + idCanilla: number; + fecha: string; // "yyyy-MM-dd" + cantSalida: number; + cantEntrada: number; + observacion?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/EntradaSalidaCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/EntradaSalidaCanillaDto.ts new file mode 100644 index 0000000..6cd645c --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/EntradaSalidaCanillaDto.ts @@ -0,0 +1,22 @@ +export interface EntradaSalidaCanillaDto { + idParte: number; + idPublicacion: number; + nombrePublicacion: string; + idCanilla: number; + nomApeCanilla: string; + canillaEsAccionista: boolean; + fecha: string; // "yyyy-MM-dd" + cantSalida: number; + cantEntrada: number; + vendidos: number; + observacion?: string | null; + liquidado: boolean; + fechaLiquidado?: string | null; // "yyyy-MM-dd" + userLiq?: number | null; + nombreUserLiq?: string | null; + montoARendir: number; + precioUnitarioAplicado: number; + recargoAplicado: number; + porcentajeOMontoCanillaAplicado: number; + esPorcentajeCanilla: boolean; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/EntradaSalidaCanillaItemDto.ts b/Frontend/src/models/dtos/Distribucion/EntradaSalidaCanillaItemDto.ts new file mode 100644 index 0000000..406998d --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/EntradaSalidaCanillaItemDto.ts @@ -0,0 +1,6 @@ +export interface EntradaSalidaCanillaItemDto { + idPublicacion: number; + cantSalida: number; + cantEntrada: number; + observacion?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/LiquidarMovimientosCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/LiquidarMovimientosCanillaDto.ts new file mode 100644 index 0000000..fe0696c --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/LiquidarMovimientosCanillaDto.ts @@ -0,0 +1,4 @@ +export interface LiquidarMovimientosCanillaRequestDto { + idsPartesALiquidar: number[]; + fechaLiquidacion: string; // "yyyy-MM-dd" +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdateControlDevolucionesDto.ts b/Frontend/src/models/dtos/Distribucion/UpdateControlDevolucionesDto.ts new file mode 100644 index 0000000..92e932c --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdateControlDevolucionesDto.ts @@ -0,0 +1,6 @@ +export interface UpdateControlDevolucionesDto { + entrada: number; + sobrantes: number; + detalle?: string | null; + sinCargo: number; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto.ts b/Frontend/src/models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto.ts new file mode 100644 index 0000000..32006fc --- /dev/null +++ b/Frontend/src/models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto.ts @@ -0,0 +1,5 @@ +export interface UpdateEntradaSalidaCanillaDto { + cantSalida: number; + cantEntrada: number; + observacion?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Radios/CancionDto.ts b/Frontend/src/models/dtos/Radios/CancionDto.ts new file mode 100644 index 0000000..4fef146 --- /dev/null +++ b/Frontend/src/models/dtos/Radios/CancionDto.ts @@ -0,0 +1,14 @@ +export interface CancionDto { + id: number; + tema?: string | null; + compositorAutor?: string | null; + interprete?: string | null; + sello?: string | null; + placa?: string | null; + pista?: number | null; + introduccion?: string | null; + idRitmo?: number | null; + nombreRitmo?: string | null; + formato?: string | null; + album?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Radios/CreateCancionDto.ts b/Frontend/src/models/dtos/Radios/CreateCancionDto.ts new file mode 100644 index 0000000..fc0f198 --- /dev/null +++ b/Frontend/src/models/dtos/Radios/CreateCancionDto.ts @@ -0,0 +1,12 @@ +export interface CreateCancionDto { + tema?: string | null; + compositorAutor?: string | null; + interprete?: string | null; + sello?: string | null; + placa?: string | null; + pista?: number | null; + introduccion?: string | null; + idRitmo?: number | null; + formato?: string | null; + album?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Radios/CreateRitmoDto.ts b/Frontend/src/models/dtos/Radios/CreateRitmoDto.ts new file mode 100644 index 0000000..d01fa45 --- /dev/null +++ b/Frontend/src/models/dtos/Radios/CreateRitmoDto.ts @@ -0,0 +1,3 @@ +export interface CreateRitmoDto { + nombreRitmo: string; // Al crear, usualmente es requerido +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Radios/GenerarListaRadioRequestDto.ts b/Frontend/src/models/dtos/Radios/GenerarListaRadioRequestDto.ts new file mode 100644 index 0000000..9ad2d22 --- /dev/null +++ b/Frontend/src/models/dtos/Radios/GenerarListaRadioRequestDto.ts @@ -0,0 +1,6 @@ +export interface GenerarListaRadioRequestDto { + mes: number; + anio: number; + institucion: "AADI" | "SADAIC"; // Tipos literales para restricción + radio: "FM 99.1" | "FM 100.3"; // Tipos literales para restricción +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Radios/RitmoDto.ts b/Frontend/src/models/dtos/Radios/RitmoDto.ts new file mode 100644 index 0000000..ef95a12 --- /dev/null +++ b/Frontend/src/models/dtos/Radios/RitmoDto.ts @@ -0,0 +1,4 @@ +export interface RitmoDto { + id: number; + nombreRitmo?: string | null; // La BD permite NULL para la columna Ritmo +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Radios/UpdateCancionDto.ts b/Frontend/src/models/dtos/Radios/UpdateCancionDto.ts new file mode 100644 index 0000000..087e62e --- /dev/null +++ b/Frontend/src/models/dtos/Radios/UpdateCancionDto.ts @@ -0,0 +1,12 @@ +export interface UpdateCancionDto { + tema?: string | null; + compositorAutor?: string | null; + interprete?: string | null; + sello?: string | null; + placa?: string | null; + pista?: number | null; + introduccion?: string | null; + idRitmo?: number | null; + formato?: string | null; + album?: string | null; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Radios/UpdateRitmoDto.ts b/Frontend/src/models/dtos/Radios/UpdateRitmoDto.ts new file mode 100644 index 0000000..4f35e73 --- /dev/null +++ b/Frontend/src/models/dtos/Radios/UpdateRitmoDto.ts @@ -0,0 +1,3 @@ +export interface UpdateRitmoDto { + nombreRitmo: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Usuarios/Auditoria/UsuarioHistorialDto.ts b/Frontend/src/models/dtos/Usuarios/Auditoria/UsuarioHistorialDto.ts new file mode 100644 index 0000000..d99ff0a --- /dev/null +++ b/Frontend/src/models/dtos/Usuarios/Auditoria/UsuarioHistorialDto.ts @@ -0,0 +1,27 @@ +export interface UsuarioHistorialDto { + idHist: number; + idUsuarioAfectado: number; + userAfectado: string; + + userAnt?: string | null; + userNvo: string; + habilitadaAnt?: boolean | null; + habilitadaNva: boolean; + supAdminAnt?: boolean | null; + supAdminNvo: boolean; + nombreAnt?: string | null; + nombreNvo: string; + apellidoAnt?: string | null; + apellidoNvo: string; + idPerfilAnt?: number | null; + idPerfilNvo: number; + nombrePerfilAnt?: string | null; + nombrePerfilNvo: string; + debeCambiarClaveAnt?: boolean | null; + debeCambiarClaveNva: boolean; + + idUsuarioModifico: number; + nombreUsuarioModifico: string; + fechaModificacion: string; // vendrá como string ISO "2023-10-27T10:30:00" + tipoModificacion: string; +} \ No newline at end of file diff --git a/Frontend/src/pages/Contables/ContablesIndexPage.tsx b/Frontend/src/pages/Contables/ContablesIndexPage.tsx index c4b5a73..0a1a624 100644 --- a/Frontend/src/pages/Contables/ContablesIndexPage.tsx +++ b/Frontend/src/pages/Contables/ContablesIndexPage.tsx @@ -6,8 +6,8 @@ import { Outlet, useNavigate, useLocation } from 'react-router-dom'; // Define las sub-pestañas del módulo Contables const contablesSubModules = [ { label: 'Tipos de Pago', path: 'tipos-pago' }, // Se convertirá en /contables/tipos-pago - // { label: 'Pagos', path: 'pagos' }, // Ejemplo de otra sub-pestaña futura - // { label: 'Créditos/Débitos', path: 'creditos-debitos' }, + { label: 'Pagos Distribuidores', path: 'pagos-distribuidores' }, + { label: 'Notas Crédito/Débito', path: 'notas-cd' }, ]; const ContablesIndexPage: React.FC = () => { diff --git a/Frontend/src/pages/Contables/GestionarNotasCDPage.tsx b/Frontend/src/pages/Contables/GestionarNotasCDPage.tsx new file mode 100644 index 0000000..52a3ec0 --- /dev/null +++ b/Frontend/src/pages/Contables/GestionarNotasCDPage.tsx @@ -0,0 +1,262 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FilterListIcon from '@mui/icons-material/FilterList'; + +import notaCreditoDebitoService from '../../services/Contables/notaCreditoDebitoService'; +import distribuidorService from '../../services/Distribucion/distribuidorService'; +import canillaService from '../../services/Distribucion/canillaService'; +import empresaService from '../../services/Distribucion/empresaService'; + +import type { NotaCreditoDebitoDto } from '../../models/dtos/Contables/NotaCreditoDebitoDto'; +import type { CreateNotaDto } from '../../models/dtos/Contables/CreateNotaDto'; +import type { UpdateNotaDto } from '../../models/dtos/Contables/UpdateNotaDto'; +import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; +import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; +import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; + +import NotaCreditoDebitoFormModal from '../../components/Modals/Contables/NotaCreditoDebitoFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +type DestinoFiltroType = 'Distribuidores' | 'Canillas' | ''; +type TipoNotaFiltroType = 'Credito' | 'Debito' | ''; + +const GestionarNotasCDPage: React.FC = () => { + const [notas, setNotas] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + // Filtros + const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); + const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); + const [filtroDestino, setFiltroDestino] = useState(''); + const [filtroIdDestinatario, setFiltroIdDestinatario] = useState(''); + const [filtroIdEmpresa, setFiltroIdEmpresa] = useState(''); + const [filtroTipoNota, setFiltroTipoNota] = useState(''); + + const [destinatariosFiltro, setDestinatariosFiltro] = useState<(DistribuidorDto | CanillaDto)[]>([]); + const [empresas, setEmpresas] = useState([]); + const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); + + const [modalOpen, setModalOpen] = useState(false); + const [editingNota, setEditingNota] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedRow, setSelectedRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + // CN001 (Ver), CN002 (Crear), CN003 (Modificar), CN004 (Eliminar) + const puedeVer = isSuperAdmin || tienePermiso("CN001"); + const puedeCrear = isSuperAdmin || tienePermiso("CN002"); + const puedeModificar = isSuperAdmin || tienePermiso("CN003"); + const puedeEliminar = isSuperAdmin || tienePermiso("CN004"); + + const fetchEmpresasParaFiltro = useCallback(async () => { + setLoadingFiltersDropdown(true); + try { + const empData = await empresaService.getAllEmpresas(); + setEmpresas(empData); + } catch (err) { console.error(err); setError("Error al cargar empresas."); + } finally { setLoadingFiltersDropdown(false); } + }, []); + + const fetchDestinatariosParaFiltro = useCallback(async (tipoDestino: DestinoFiltroType) => { + if (!tipoDestino) { setDestinatariosFiltro([]); return; } + setLoadingFiltersDropdown(true); + setFiltroIdDestinatario(''); // Resetear selección de destinatario + try { + if (tipoDestino === 'Distribuidores') { + const data = await distribuidorService.getAllDistribuidores(); + setDestinatariosFiltro(data); + } else if (tipoDestino === 'Canillas') { + const data = await canillaService.getAllCanillas(undefined, undefined, true); + setDestinatariosFiltro(data); + } + } catch (err) { console.error(err); setError(`Error al cargar ${tipoDestino}.`); + } finally { setLoadingFiltersDropdown(false); } + }, []); + + useEffect(() => { fetchEmpresasParaFiltro(); }, [fetchEmpresasParaFiltro]); + useEffect(() => { fetchDestinatariosParaFiltro(filtroDestino); }, [filtroDestino, fetchDestinatariosParaFiltro]); + + + const cargarNotas = useCallback(async () => { + if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const params = { + fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null, + destino: filtroDestino || null, + idDestino: filtroIdDestinatario ? Number(filtroIdDestinatario) : null, + idEmpresa: filtroIdEmpresa ? Number(filtroIdEmpresa) : null, + tipoNota: filtroTipoNota || null, + }; + const data = await notaCreditoDebitoService.getAllNotas(params); + setNotas(data); + } catch (err) { console.error(err); setError('Error al cargar las notas.'); + } finally { setLoading(false); } + }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroDestino, filtroIdDestinatario, filtroIdEmpresa, filtroTipoNota]); + + useEffect(() => { cargarNotas(); }, [cargarNotas]); + + const handleOpenModal = (item?: NotaCreditoDebitoDto) => { + setEditingNota(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { setModalOpen(false); setEditingNota(null); }; + + const handleSubmitModal = async (data: CreateNotaDto | UpdateNotaDto, idNota?: number) => { + setApiErrorMessage(null); + try { + if (idNota && editingNota) { + await notaCreditoDebitoService.updateNota(idNota, data as UpdateNotaDto); + } else { + await notaCreditoDebitoService.createNota(data as CreateNotaDto); + } + cargarNotas(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la nota.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (idNota: number) => { + if (window.confirm(`¿Seguro de eliminar esta nota (ID: ${idNota})? Esta acción revertirá el impacto en el saldo.`)) { + setApiErrorMessage(null); + try { await notaCreditoDebitoService.deleteNota(idNota); cargarNotas(); } + catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: NotaCreditoDebitoDto) => { + setAnchorEl(event.currentTarget); setSelectedRow(item); + }; + const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = notas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; + + if (!loading && !puedeVer && !loadingFiltersDropdown) return {error || "Acceso denegado."}; + + return ( + + Notas de Crédito/Débito + + Filtros + + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> + + Empresa + + + + Tipo Destino + + + + Destinatario + + + + Tipo Nota + + + + {puedeCrear && ()} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + + + FechaEmpresaDestino + DestinatarioTipo + MontoReferenciaObs. + {(puedeModificar || puedeEliminar) && Acciones} + + + {displayData.length === 0 ? ( + No se encontraron notas. + ) : ( + displayData.map((n) => ( + + {formatDate(n.fecha)}{n.nombreEmpresa} + {n.destino}{n.nombreDestinatario} + + + + ${n.monto.toFixed(2)} + {n.referencia || '-'} + {n.observaciones || '-'} + {(puedeModificar || puedeEliminar) && ( + + handleMenuOpen(e, n)} disabled={!puedeModificar && !puedeEliminar}> + + )} + + )))} + +
+ +
+ )} + + + {puedeModificar && selectedRow && ( + { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} + {puedeEliminar && selectedRow && ( + handleDelete(selectedRow.idNota)}> Eliminar)} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarNotasCDPage; \ No newline at end of file diff --git a/Frontend/src/pages/Contables/GestionarPagosDistribuidorPage.tsx b/Frontend/src/pages/Contables/GestionarPagosDistribuidorPage.tsx new file mode 100644 index 0000000..67743d8 --- /dev/null +++ b/Frontend/src/pages/Contables/GestionarPagosDistribuidorPage.tsx @@ -0,0 +1,231 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FilterListIcon from '@mui/icons-material/FilterList'; + +import pagoDistribuidorService from '../../services/Contables/pagoDistribuidorService'; +import distribuidorService from '../../services/Distribucion/distribuidorService'; +import empresaService from '../../services/Distribucion/empresaService'; + +import type { PagoDistribuidorDto } from '../../models/dtos/Contables/PagoDistribuidorDto'; +import type { CreatePagoDistribuidorDto } from '../../models/dtos/Contables/CreatePagoDistribuidorDto'; +import type { UpdatePagoDistribuidorDto } from '../../models/dtos/Contables/UpdatePagoDistribuidorDto'; +import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; +import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; + +import PagoDistribuidorFormModal from '../../components/Modals/Contables/PagoDistribuidorFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarPagosDistribuidorPage: React.FC = () => { + const [pagos, setPagos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + // Filtros + const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); + const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); + const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState(''); + const [filtroIdEmpresa, setFiltroIdEmpresa] = useState(''); + const [filtroTipoMov, setFiltroTipoMov] = useState<'Recibido' | 'Realizado' | ''>(''); + + const [distribuidores, setDistribuidores] = useState([]); + const [empresas, setEmpresas] = useState([]); + const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); + + const [modalOpen, setModalOpen] = useState(false); + const [editingPago, setEditingPago] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedRow, setSelectedRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + // Permisos CP001 (Ver), CP002 (Crear), CP003 (Modificar), CP004 (Eliminar) + const puedeVer = isSuperAdmin || tienePermiso("CP001"); + const puedeCrear = isSuperAdmin || tienePermiso("CP002"); + const puedeModificar = isSuperAdmin || tienePermiso("CP003"); + const puedeEliminar = isSuperAdmin || tienePermiso("CP004"); + + const fetchFiltersDropdownData = useCallback(async () => { + setLoadingFiltersDropdown(true); + try { + const [distData, empData] = await Promise.all([ + distribuidorService.getAllDistribuidores(), + empresaService.getAllEmpresas() + ]); + setDistribuidores(distData); + setEmpresas(empData); + } catch (err) { console.error(err); setError("Error al cargar opciones de filtro."); + } finally { setLoadingFiltersDropdown(false); } + }, []); + + useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); + + const cargarPagos = useCallback(async () => { + if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const params = { + fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null, + idDistribuidor: filtroIdDistribuidor ? Number(filtroIdDistribuidor) : null, + idEmpresa: filtroIdEmpresa ? Number(filtroIdEmpresa) : null, + tipoMovimiento: filtroTipoMov || null, + }; + const data = await pagoDistribuidorService.getAllPagosDistribuidor(params); + setPagos(data); + } catch (err) { console.error(err); setError('Error al cargar los pagos.'); + } finally { setLoading(false); } + }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdDistribuidor, filtroIdEmpresa, filtroTipoMov]); + + useEffect(() => { cargarPagos(); }, [cargarPagos]); + + const handleOpenModal = (item?: PagoDistribuidorDto) => { + setEditingPago(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { setModalOpen(false); setEditingPago(null); }; + + const handleSubmitModal = async (data: CreatePagoDistribuidorDto | UpdatePagoDistribuidorDto, idPago?: number) => { + setApiErrorMessage(null); + try { + if (idPago && editingPago) { + await pagoDistribuidorService.updatePagoDistribuidor(idPago, data as UpdatePagoDistribuidorDto); + } else { + await pagoDistribuidorService.createPagoDistribuidor(data as CreatePagoDistribuidorDto); + } + cargarPagos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el pago.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (idPago: number) => { + if (window.confirm(`¿Seguro de eliminar este pago (ID: ${idPago})? Esta acción revertirá el impacto en el saldo.`)) { + setApiErrorMessage(null); + try { await pagoDistribuidorService.deletePagoDistribuidor(idPago); cargarPagos(); } + catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: PagoDistribuidorDto) => { + setAnchorEl(event.currentTarget); setSelectedRow(item); + }; + const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = pagos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; + + if (!loading && !puedeVer && !loadingFiltersDropdown) return {error || "Acceso denegado."}; + + return ( + + Pagos de Distribuidores + + Filtros + + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> + + Distribuidor + + + + Empresa (Saldo) + + + + Tipo Mov. + + + + {puedeCrear && ()} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + + + FechaDistribuidorEmpresa (Saldo) + Tipo Mov.Recibo N° + MontoTipo Pago + Detalle + {(puedeModificar || puedeEliminar) && Acciones} + + + {displayData.length === 0 ? ( + No se encontraron pagos. + ) : ( + displayData.map((p) => ( + + {formatDate(p.fecha)}{p.nombreDistribuidor} + {p.nombreEmpresa} + + + + {p.recibo} + ${p.monto.toFixed(2)} + {p.nombreTipoPago} + {p.detalle || '-'} + {(puedeModificar || puedeEliminar) && ( + + handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar}> + + )} + + )))} + +
+ +
+ )} + + + {puedeModificar && selectedRow && ( + { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} + {puedeEliminar && selectedRow && ( + handleDelete(selectedRow.idPago)}> Eliminar)} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarPagosDistribuidorPage; \ No newline at end of file diff --git a/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx b/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx index 5b973cf..db90a27 100644 --- a/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx +++ b/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx @@ -9,8 +9,8 @@ import AddIcon from '@mui/icons-material/Add'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import tipoPagoService from '../../services/Contables/tipoPagoService'; import type { TipoPago } from '../../models/Entities/TipoPago'; -import type { CreateTipoPagoDto } from '../../models/dtos/tiposPago/CreateTipoPagoDto'; -import type { UpdateTipoPagoDto } from '../../models/dtos/tiposPago/UpdateTipoPagoDto'; +import type { CreateTipoPagoDto } from '../../models/dtos/Contables/CreateTipoPagoDto'; +import type { UpdateTipoPagoDto } from '../../models/dtos/Contables/UpdateTipoPagoDto'; import TipoPagoFormModal from '../../components/Modals/Contables/TipoPagoFormModal'; import axios from 'axios'; import { usePermissions } from '../../hooks/usePermissions'; diff --git a/Frontend/src/pages/Distribucion/ESCanillasPage.tsx b/Frontend/src/pages/Distribucion/ESCanillasPage.tsx deleted file mode 100644 index 19b77fd..0000000 --- a/Frontend/src/pages/Distribucion/ESCanillasPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; - -const ESCanillasPage: React.FC = () => { - return Página de Gestión de E/S de Canillas; -}; -export default ESCanillasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarControlDevolucionesPage.tsx b/Frontend/src/pages/Distribucion/GestionarControlDevolucionesPage.tsx new file mode 100644 index 0000000..4931dc7 --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarControlDevolucionesPage.tsx @@ -0,0 +1,220 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FilterListIcon from '@mui/icons-material/FilterList'; + +import controlDevolucionesService from '../../services/Distribucion/controlDevolucionesService'; +import empresaService from '../../services/Distribucion/empresaService'; // Para el filtro de empresa + +import type { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto'; +import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto'; +import type { UpdateControlDevolucionesDto } from '../../models/dtos/Distribucion/UpdateControlDevolucionesDto'; +import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; + +import ControlDevolucionesFormModal from '../../components/Modals/Distribucion/ControlDevolucionesFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarControlDevolucionesPage: React.FC = () => { + const [controles, setControles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + // Filtros + const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); + const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); + const [filtroIdEmpresa, setFiltroIdEmpresa] = useState(''); + + const [empresas, setEmpresas] = useState([]); + const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); + + const [modalOpen, setModalOpen] = useState(false); + const [editingControl, setEditingControl] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedRow, setSelectedRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + // Permisos CD001 (Ver), CD002 (Crear), CD003 (Modificar) + const puedeVer = isSuperAdmin || tienePermiso("CD001"); + const puedeCrear = isSuperAdmin || tienePermiso("CD002"); + const puedeModificar = isSuperAdmin || tienePermiso("CD003"); + const puedeEliminar = isSuperAdmin || tienePermiso("CD003"); // Asumiendo que modificar incluye eliminar + + const fetchFiltersDropdownData = useCallback(async () => { + setLoadingFiltersDropdown(true); + try { + const empresasData = await empresaService.getAllEmpresas(); + setEmpresas(empresasData); + } catch (err) { + console.error("Error cargando empresas para filtro:", err); + setError("Error al cargar opciones de filtro."); + } finally { + setLoadingFiltersDropdown(false); + } + }, []); + + useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); + + const cargarControles = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); setLoading(false); return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const params = { + fechaDesde: filtroFechaDesde || null, + fechaHasta: filtroFechaHasta || null, + idEmpresa: filtroIdEmpresa ? Number(filtroIdEmpresa) : null, + }; + const data = await controlDevolucionesService.getAllControlesDevoluciones(params); + setControles(data); + } catch (err) { + console.error(err); setError('Error al cargar los controles de devoluciones.'); + } finally { setLoading(false); } + }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdEmpresa]); + + useEffect(() => { cargarControles(); }, [cargarControles]); + + const handleOpenModal = (item?: ControlDevolucionesDto) => { + setEditingControl(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingControl(null); + }; + + const handleSubmitModal = async (data: CreateControlDevolucionesDto | UpdateControlDevolucionesDto, idControl?: number) => { + setApiErrorMessage(null); + try { + if (idControl && editingControl) { + await controlDevolucionesService.updateControlDevoluciones(idControl, data as UpdateControlDevolucionesDto); + } else { + await controlDevolucionesService.createControlDevoluciones(data as CreateControlDevolucionesDto); + } + cargarControles(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el control.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (idControl: number) => { + if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) { + setApiErrorMessage(null); + try { + await controlDevolucionesService.deleteControlDevoluciones(idControl); + cargarControles(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: ControlDevolucionesDto) => { + setAnchorEl(event.currentTarget); setSelectedRow(item); + }; + const handleMenuClose = () => { + setAnchorEl(null); setSelectedRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = controles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; + + if (!loading && !puedeVer && !loadingFiltersDropdown) return {error || "Acceso denegado."}; + + return ( + + Control de Devoluciones a Empresa + + Filtros + + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> + + Empresa + + + + {puedeCrear && ()} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + + + FechaEmpresa + Entrada (Total Dev.) + Sobrantes + Sin Cargo + Detalle + {(puedeModificar || puedeEliminar) && Acciones} + + + {displayData.length === 0 ? ( + No se encontraron controles. + ) : ( + displayData.map((c) => ( + + {formatDate(c.fecha)} + {c.nombreEmpresa} + {c.entrada} + {c.sobrantes} + {c.sinCargo} + {c.detalle || '-'} + {(puedeModificar || puedeEliminar) && ( + + handleMenuOpen(e, c)} disabled={!puedeModificar && !puedeEliminar}> + + )} + + )))} + +
+ +
+ )} + + + {puedeModificar && selectedRow && ( + { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} + {puedeEliminar && selectedRow && ( + handleDelete(selectedRow.idControl)}> Eliminar)} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarControlDevolucionesPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx b/Frontend/src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx new file mode 100644 index 0000000..b679426 --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx @@ -0,0 +1,369 @@ +// src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, FormControl, InputLabel, Select, Checkbox, Tooltip, + Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FilterListIcon from '@mui/icons-material/FilterList'; +import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck'; // Para Liquidar + +import entradaSalidaCanillaService from '../../services/Distribucion/entradaSalidaCanillaService'; +import publicacionService from '../../services/Distribucion/publicacionService'; +import canillaService from '../../services/Distribucion/canillaService'; + +import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto'; +import type { CreateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaCanillaDto'; +import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto'; +import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; +import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; +import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto'; + + +import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarEntradasSalidasCanillaPage: React.FC = () => { + const [movimientos, setMovimientos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + // Filtros + const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); + const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); + const [filtroIdPublicacion, setFiltroIdPublicacion] = useState(''); + const [filtroIdCanilla, setFiltroIdCanilla] = useState(''); + const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados'); + + + const [publicaciones, setPublicaciones] = useState([]); + const [canillitas, setCanillitas] = useState([]); + const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); + + const [modalOpen, setModalOpen] = useState(false); + const [editingMovimiento, setEditingMovimiento] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedRow, setSelectedRow] = useState(null); + const [selectedIdsParaLiquidar, setSelectedIdsParaLiquidar] = useState>(new Set()); + const [fechaLiquidacionDialog, setFechaLiquidacionDialog] = useState(new Date().toISOString().split('T')[0]); + const [openLiquidarDialog, setOpenLiquidarDialog] = useState(false); + + + const { tienePermiso, isSuperAdmin } = usePermissions(); + // MC001 (Ver), MC002 (Crear), MC003 (Modificar), MC004 (Eliminar), MC005 (Liquidar) + const puedeVer = isSuperAdmin || tienePermiso("MC001"); + const puedeCrear = isSuperAdmin || tienePermiso("MC002"); + const puedeModificar = isSuperAdmin || tienePermiso("MC003"); + const puedeEliminar = isSuperAdmin || tienePermiso("MC004"); + const puedeLiquidar = isSuperAdmin || tienePermiso("MC005"); + const puedeEliminarLiquidados = isSuperAdmin || tienePermiso("MC006"); + + const fetchFiltersDropdownData = useCallback(async () => { + setLoadingFiltersDropdown(true); + try { + const [pubsData, canData] = await Promise.all([ + publicacionService.getAllPublicaciones(undefined, undefined, true), + canillaService.getAllCanillas(undefined, undefined, true) + ]); + setPublicaciones(pubsData); + setCanillitas(canData); + } catch (err) { + console.error(err); setError("Error al cargar opciones de filtro."); + } finally { setLoadingFiltersDropdown(false); } + }, []); + + useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); + + const cargarMovimientos = useCallback(async () => { + if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + let liquidadosFilter: boolean | null = null; + let incluirNoLiquidadosFilter: boolean | null = true; // Por defecto mostrar no liquidados + + if (filtroEstadoLiquidacion === 'liquidados') { + liquidadosFilter = true; + incluirNoLiquidadosFilter = false; + } else if (filtroEstadoLiquidacion === 'noLiquidados') { + liquidadosFilter = false; + incluirNoLiquidadosFilter = true; + } // Si es 'todos', ambos son null o true y false respectivamente (backend debe manejarlo) + + + const params = { + fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null, + idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null, + idCanilla: filtroIdCanilla ? Number(filtroIdCanilla) : null, + liquidados: liquidadosFilter, + incluirNoLiquidados: filtroEstadoLiquidacion === 'todos' ? null : incluirNoLiquidadosFilter, + }; + const data = await entradaSalidaCanillaService.getAllEntradasSalidasCanilla(params); + setMovimientos(data); + setSelectedIdsParaLiquidar(new Set()); // Limpiar selección al recargar + } catch (err) { + console.error(err); setError('Error al cargar movimientos.'); + } finally { setLoading(false); } + }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdCanilla, filtroEstadoLiquidacion]); + + useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]); + + const handleOpenModal = (item?: EntradaSalidaCanillaDto) => { + setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { setModalOpen(false); setEditingMovimiento(null); }; + + const handleSubmitModal = async (data: CreateEntradaSalidaCanillaDto | UpdateEntradaSalidaCanillaDto, idParte?: number) => { + setApiErrorMessage(null); + try { + if (idParte && editingMovimiento) { + await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data as UpdateEntradaSalidaCanillaDto); + } else { + await entradaSalidaCanillaService.createEntradaSalidaCanilla(data as CreateEntradaSalidaCanillaDto); + } + cargarMovimientos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (idParte: number) => { + if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) { + setApiErrorMessage(null); + try { await entradaSalidaCanillaService.deleteEntradaSalidaCanilla(idParte); cargarMovimientos(); } + catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: EntradaSalidaCanillaDto) => { + setAnchorEl(event.currentTarget); setSelectedRow(item); + }; + const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); }; + + const handleSelectRowForLiquidar = (idParte: number) => { + setSelectedIdsParaLiquidar(prev => { + const newSet = new Set(prev); + if (newSet.has(idParte)) newSet.delete(idParte); + else newSet.add(idParte); + return newSet; + }); + }; + const handleSelectAllForLiquidar = (event: React.ChangeEvent) => { + if (event.target.checked) { + const newSelectedIds = new Set(movimientos.filter(m => !m.liquidado).map(m => m.idParte)); + setSelectedIdsParaLiquidar(newSelectedIds); + } else { + setSelectedIdsParaLiquidar(new Set()); + } + }; + + const handleOpenLiquidarDialog = () => { + if (selectedIdsParaLiquidar.size === 0) { + setApiErrorMessage("Seleccione al menos un movimiento para liquidar."); + return; + } + setOpenLiquidarDialog(true); + }; + const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false); + const handleConfirmLiquidar = async () => { + setApiErrorMessage(null); setLoading(true); + const liquidarDto: LiquidarMovimientosCanillaRequestDto = { + idsPartesALiquidar: Array.from(selectedIdsParaLiquidar), + fechaLiquidacion: fechaLiquidacionDialog + }; + try { + await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto); + cargarMovimientos(); // Recargar para ver los cambios + setOpenLiquidarDialog(false); + } catch (err: any) { + const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al liquidar.'; + setApiErrorMessage(msg); + } finally { + setLoading(false); + } + }; + + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; + + if (!loading && !puedeVer && !loadingFiltersDropdown) return {error || "Acceso denegado."}; + + const numSelectedToLiquidate = selectedIdsParaLiquidar.size; + const numNotLiquidatedOnPage = displayData.filter(m => !m.liquidado).length; + + + return ( + + Entradas/Salidas Canillitas + + Filtros + + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} /> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} /> + + Publicación + + + + Canillita + + + + Estado Liquidación + + + + + {puedeCrear && ()} + {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && numSelectedToLiquidate > 0 && ( + + )} + + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeVer && ( + + + + {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && + + 0 && numSelectedToLiquidate < numNotLiquidatedOnPage} + checked={numNotLiquidatedOnPage > 0 && numSelectedToLiquidate === numNotLiquidatedOnPage} + onChange={handleSelectAllForLiquidar} + disabled={numNotLiquidatedOnPage === 0} + /> + + } + FechaPublicaciónCanillita + SalidaEntrada + VendidosA Rendir + LiquidadoF. Liq.Obs. + {(puedeModificar || puedeEliminar) && Acciones} + + + {displayData.length === 0 ? ( + No se encontraron movimientos. + ) : ( + displayData.map((m) => ( + + {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && + + handleSelectRowForLiquidar(m.idParte)} + disabled={m.liquidado} + /> + + } + {formatDate(m.fecha)} + {m.nombrePublicacion} + {m.nomApeCanilla} + {m.cantSalida} + {m.cantEntrada} + {m.vendidos} + ${m.montoARendir.toFixed(2)} + {m.liquidado ? : } + {m.fechaLiquidado ? formatDate(m.fechaLiquidado) : '-'} + {m.observacion || '-'} + {(puedeModificar || puedeEliminar) && ( + + handleMenuOpen(e, m)} + disabled={ + // Deshabilitar si no tiene ningún permiso de eliminación O + // si está liquidado y no tiene permiso para eliminar liquidados + !((!m.liquidado && puedeEliminar) || (m.liquidado && puedeEliminarLiquidados)) + } + > + + + + )} + + )))} + +
+ +
+ )} + + + {puedeModificar && selectedRow && !selectedRow.liquidado && ( + { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} + {selectedRow && ( + (!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados) + ) && ( + handleDelete(selectedRow.idParte)}> + Eliminar + + )} + + + setApiErrorMessage(null)} + /> + + + Confirmar Liquidación + + + Se marcarán como liquidados {selectedIdsParaLiquidar.size} movimiento(s). + + setFechaLiquidacionDialog(e.target.value)} + InputLabelProps={{ shrink: true }} + /> + + + + + + + +
+ ); +}; + +export default GestionarEntradasSalidasCanillaPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx b/Frontend/src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx index f14671f..954f63d 100644 --- a/Frontend/src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx +++ b/Frontend/src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx @@ -1,8 +1,9 @@ +// src/pages/Distribucion/GestionarEntradasSalidasDistPage.tsx import React, { useState, useEffect, useCallback } from 'react'; import { - Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, - CircularProgress, Alert, FormControl, InputLabel, Select + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import MoreVertIcon from '@mui/icons-material/MoreVert'; @@ -52,36 +53,30 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { const { tienePermiso, isSuperAdmin } = usePermissions(); const puedeVer = isSuperAdmin || tienePermiso("MD001"); - const puedeGestionar = isSuperAdmin || tienePermiso("MD002"); // Para Crear, Editar, Eliminar + const puedeGestionar = isSuperAdmin || tienePermiso("MD002"); const fetchFiltersDropdownData = useCallback(async () => { setLoadingFiltersDropdown(true); try { - const [pubsData, distData] = await Promise.all([ - publicacionService.getAllPublicaciones(undefined, undefined, true), - distribuidorService.getAllDistribuidores() - ]); - setPublicaciones(pubsData); - setDistribuidores(distData); + const [pubsData, distData] = await Promise.all([ + publicacionService.getAllPublicaciones(undefined, undefined, true), + distribuidorService.getAllDistribuidores() + ]); + setPublicaciones(pubsData); + setDistribuidores(distData); } catch (err) { - console.error("Error cargando datos para filtros:", err); - setError("Error al cargar opciones de filtro."); - } finally { - setLoadingFiltersDropdown(false); - } + console.error(err); setError("Error al cargar opciones de filtro."); + } finally { setLoadingFiltersDropdown(false); } }, []); useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); const cargarMovimientos = useCallback(async () => { - if (!puedeVer) { - setError("No tiene permiso para ver esta sección."); setLoading(false); return; - } + if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; } setLoading(true); setError(null); setApiErrorMessage(null); try { const params = { - fechaDesde: filtroFechaDesde || null, - fechaHasta: filtroFechaHasta || null, + fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null, idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null, idDistribuidor: filtroIdDistribuidor ? Number(filtroIdDistribuidor) : null, tipoMovimiento: filtroTipoMov || null, @@ -89,7 +84,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { const data = await entradaSalidaDistService.getAllEntradasSalidasDist(params); setMovimientos(data); } catch (err) { - console.error(err); setError('Error al cargar los movimientos.'); + console.error(err); setError('Error al cargar movimientos.'); } finally { setLoading(false); } }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdDistribuidor, filtroTipoMov]); @@ -98,9 +93,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { const handleOpenModal = (item?: EntradaSalidaDistDto) => { setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true); }; - const handleCloseModal = () => { - setModalOpen(false); setEditingMovimiento(null); - }; + const handleCloseModal = () => { setModalOpen(false); setEditingMovimiento(null); }; const handleSubmitModal = async (data: CreateEntradaSalidaDistDto | UpdateEntradaSalidaDistDto, idParte?: number) => { setApiErrorMessage(null); @@ -112,21 +105,16 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { } cargarMovimientos(); } catch (err: any) { - const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el movimiento.'; + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.'; setApiErrorMessage(message); throw err; } }; const handleDelete = async (idParte: number) => { - if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})? Esta acción revertirá el impacto en el saldo del distribuidor.`)) { - setApiErrorMessage(null); - try { - await entradaSalidaDistService.deleteEntradaSalidaDist(idParte); - cargarMovimientos(); - } catch (err: any) { - const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; - setApiErrorMessage(message); - } + if (window.confirm(`¿Seguro (ID: ${idParte})? Esto revertirá el saldo.`)) { + setApiErrorMessage(null); + try { await entradaSalidaDistService.deleteEntradaSalidaDist(idParte); cargarMovimientos(); } + catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); } } handleMenuClose(); }; @@ -134,9 +122,7 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { const handleMenuOpen = (event: React.MouseEvent, item: EntradaSalidaDistDto) => { setAnchorEl(event.currentTarget); setSelectedRow(item); }; - const handleMenuClose = () => { - setAnchorEl(null); setSelectedRow(null); - }; + const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); }; const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); const handleChangeRowsPerPage = (event: React.ChangeEvent) => { @@ -145,97 +131,96 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; - if (!loading && !puedeVer && !loadingFiltersDropdown) return {error || "Acceso denegado."}; return ( - Entradas/Salidas Distribuidores + Entradas/Salidas a Distribuidores - Filtros - - setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> - setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> - - Publicación - - - - Distribuidor - - - - Tipo - - - - {puedeGestionar && ()} + Filtros + + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} /> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} /> + + Publicación + + + + Distribuidor + + + + Tipo + + + + {puedeGestionar && ()} {loading && } - {error && !loading && {error}} - {apiErrorMessage && {apiErrorMessage}} + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} {!loading && !error && puedeVer && ( - - - - FechaPublicación (Empresa) - DistribuidorTipo - CantidadRemito - Monto AfectadoObs. - {puedeGestionar && Acciones} - - - {displayData.length === 0 ? ( - No se encontraron movimientos. - ) : ( - displayData.map((m) => ( - - {formatDate(m.fecha)} - {m.nombrePublicacion} ({m.nombreEmpresaPublicacion}) - {m.nombreDistribuidor} - - - - {m.cantidad} - {m.remito} - 0 ? 'red' : 'inherit')}}> - ${m.montoCalculado.toFixed(2)} - - {m.observacion || '-'} - {puedeGestionar && ( - - handleMenuOpen(e, m)} disabled={!puedeGestionar}> - - )} - - )))} - -
- -
- )} + + + + FechaPublicación (Empresa) + DistribuidorTipo + CantidadRemito + Monto AfectadoObs. + {puedeGestionar && Acciones} + + + {displayData.length === 0 ? ( + No se encontraron movimientos. + ) : ( + displayData.map((m) => ( + + {formatDate(m.fecha)} + {m.nombrePublicacion} + {m.nombreDistribuidor} + + + + {m.cantidad} + {m.remito} + + {m.tipoMovimiento === 'Salida' ? '$'+m.montoCalculado.toFixed(2) : '$-'+m.montoCalculado.toFixed(2) } + + {m.observacion || '-'} + {puedeGestionar && ( + + handleMenuOpen(e, m)} disabled={!puedeGestionar}> + + )} + + )))} + +
+ +
+ )} {puedeGestionar && selectedRow && ( - { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} - {puedeGestionar && selectedRow && ( // O un permiso más específico si "eliminar" es diferente de "modificar" - handleDelete(selectedRow.idParte)}> Eliminar)} + { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} + {puedeGestionar && selectedRow && ( + handleDelete(selectedRow.idParte)}> Eliminar)} { + const currentYear = new Date().getFullYear(); + const currentMonth = new Date().getMonth() + 1; // Meses son 0-indexados + + const [mes, setMes] = useState(currentMonth.toString()); + const [anio, setAnio] = useState(currentYear.toString()); + const [institucion, setInstitucion] = useState<"AADI" | "SADAIC">("AADI"); + const [radio, setRadio] = useState<"FM 99.1" | "FM 100.3">("FM 99.1"); + + const [loading, setLoading] = useState(false); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + const [apiSuccessMessage, setApiSuccessMessage] = useState(null); + const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + // Usar el permiso general de la sección Radios (SS005) o uno específico + const puedeGenerar = isSuperAdmin || tienePermiso("SS005"); + + const validate = (): boolean => { + const errors: { [key: string]: string | null } = {}; + const numMes = parseInt(mes, 10); + const numAnio = parseInt(anio, 10); + + if (!mes.trim() || isNaN(numMes) || numMes < 1 || numMes > 12) { + errors.mes = 'Mes debe ser un número entre 1 y 12.'; + } + if (!anio.trim() || isNaN(numAnio) || numAnio < 2000 || numAnio > 2999) { + errors.anio = 'Año debe ser válido (ej: 2024).'; + } + if (!institucion) errors.institucion = 'Institución es obligatoria.'; // Aunque el estado tiene tipo, la validación es buena + if (!radio) errors.radio = 'Radio es obligatoria.'; + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleGenerarLista = async () => { + clearMessages(); + if (!validate()) return; + + setLoading(true); + try { + const params: GenerarListaRadioRequestDto = { + mes: parseInt(mes, 10), + anio: parseInt(anio, 10), + institucion, + radio, + }; + + const blob = await radioListaService.generarListaRadio(params); + + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + + // Construir el nombre de archivo como en VB.NET para el zip + // Ejemplo AADI-FM99.1-FM-0524.xlsx.zip + const mesTexto = params.mes.toString().padStart(2, '0'); + const anioCortoTexto = (params.anio % 100).toString().padStart(2, '0'); + let baseFileName = ""; + if (params.institucion === "AADI") { + baseFileName = params.radio === "FM 99.1" ? `AADI-FM99.1-FM-${mesTexto}${anioCortoTexto}` : `AADI-FM100.3-FM-${mesTexto}${anioCortoTexto}`; + } else { // SADAIC + baseFileName = params.radio === "FM 99.1" ? `FM99.1-FM-${mesTexto}${anioCortoTexto}` : `FM100.3-FM-${mesTexto}${anioCortoTexto}`; + } + const defaultFileName = `${baseFileName}.zip`; // Nombre final del ZIP + + link.setAttribute('download', defaultFileName); + document.body.appendChild(link); + link.click(); + link.parentNode?.removeChild(link); + window.URL.revokeObjectURL(url); // Liberar el objeto URL + setApiSuccessMessage('Lista generada y descarga iniciada.'); + + } catch (err: any) { + console.error("Error al generar lista:", err); + if (axios.isAxiosError(err) && err.response) { + if (err.response.data instanceof Blob && err.response.data.type === "application/json") { + // Intentar leer el error JSON del Blob + const errorJson = await err.response.data.text(); + try { + const parsedError = JSON.parse(errorJson); + setApiErrorMessage(parsedError.message || 'Error al generar la lista.'); + } catch (parseError) { + setApiErrorMessage('Error al generar la lista. Respuesta de error no válida.'); + } + } else { + setApiErrorMessage(err.response.data?.message || err.message || 'Error desconocido al generar la lista.'); + } + } else { + setApiErrorMessage('Error al generar la lista.'); + } + } finally { + setLoading(false); + } + }; + + const clearMessages = () => { + setApiErrorMessage(null); + setApiSuccessMessage(null); + }; + const handleInputChange = (fieldName: string) => { + if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); + clearMessages(); + }; + + + if (!puedeGenerar) { + return Acceso denegado.; + } + + return ( + + Generar Listas de Radio + + Criterios de Generación + {/* Aumentado el gap */} + { setMes(e.target.value); handleInputChange('mes'); }} + error={!!localErrors.mes} + helperText={localErrors.mes || ''} + InputLabelProps={{ shrink: true }} + inputProps={{ min: 1, max: 12 }} + required + fullWidth + /> + { setAnio(e.target.value); handleInputChange('anio'); }} + error={!!localErrors.anio} + helperText={localErrors.anio || ''} + InputLabelProps={{ shrink: true }} + inputProps={{ min: 2000, max: 2099 }} + required + fullWidth + /> + + Institución + + {localErrors.institucion && {localErrors.institucion}} + + + + Radio + + {localErrors.radio && {localErrors.radio}} + + + + + + + {loading && } + {apiErrorMessage && {apiErrorMessage}} + {apiSuccessMessage && {apiSuccessMessage}} + + + ); +}; + +export default GestionarListasRadioPage; \ No newline at end of file diff --git a/Frontend/src/pages/Radios/GestionarCancionesPage.tsx b/Frontend/src/pages/Radios/GestionarCancionesPage.tsx new file mode 100644 index 0000000..6553362 --- /dev/null +++ b/Frontend/src/pages/Radios/GestionarCancionesPage.tsx @@ -0,0 +1,195 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert, FormControl, InputLabel, Select +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FilterListIcon from '@mui/icons-material/FilterList'; + +import cancionService from '../../services/Radios/cancionService'; +import ritmoService from '../../services/Radios/ritmoService'; // Para el filtro de ritmo + +import type { CancionDto } from '../../models/dtos/Radios/CancionDto'; +import type { CreateCancionDto } from '../../models/dtos/Radios/CreateCancionDto'; +import type { UpdateCancionDto } from '../../models/dtos/Radios/UpdateCancionDto'; +import type { RitmoDto } from '../../models/dtos/Radios/RitmoDto'; + +import CancionFormModal from '../../components/Modals/Radios/CancionFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarCancionesPage: React.FC = () => { + const [canciones, setCanciones] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + // Filtros + const [filtroTema, setFiltroTema] = useState(''); + const [filtroInterprete, setFiltroInterprete] = useState(''); + const [filtroIdRitmo, setFiltroIdRitmo] = useState(''); + + const [ritmos, setRitmos] = useState([]); // Para el dropdown de filtro + const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); + + const [modalOpen, setModalOpen] = useState(false); + const [editingCancion, setEditingCancion] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedRow, setSelectedRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeGestionar = isSuperAdmin || tienePermiso("SS005"); // Usar permiso general de la sección + + const fetchRitmosParaFiltro = useCallback(async () => { + setLoadingFiltersDropdown(true); + try { + const data = await ritmoService.getAllRitmos(); + setRitmos(data); + } catch (err) { console.error(err); setError("Error al cargar ritmos para filtro."); + } finally { setLoadingFiltersDropdown(false); } + }, []); + + useEffect(() => { fetchRitmosParaFiltro(); }, [fetchRitmosParaFiltro]); + + const cargarCanciones = useCallback(async () => { + if (!puedeGestionar) { setError("No tiene permiso."); setLoading(false); return; } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const params = { + temaFilter: filtroTema || null, + interpreteFilter: filtroInterprete || null, + idRitmoFilter: filtroIdRitmo ? Number(filtroIdRitmo) : null, + }; + const data = await cancionService.getAllCanciones(params); + setCanciones(data); + } catch (err) { console.error(err); setError('Error al cargar las canciones.'); + } finally { setLoading(false); } + }, [puedeGestionar, filtroTema, filtroInterprete, filtroIdRitmo]); + + useEffect(() => { cargarCanciones(); }, [cargarCanciones]); + + const handleOpenModal = (item?: CancionDto) => { + setEditingCancion(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { setModalOpen(false); setEditingCancion(null); }; + + const handleSubmitModal = async (data: CreateCancionDto | UpdateCancionDto, id?: number) => { + setApiErrorMessage(null); + try { + if (id && editingCancion) { + await cancionService.updateCancion(id, data as UpdateCancionDto); + } else { + await cancionService.createCancion(data as CreateCancionDto); + } + cargarCanciones(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar la canción.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm(`¿Seguro de eliminar esta canción (ID: ${id})?`)) { + setApiErrorMessage(null); + try { await cancionService.deleteCancion(id); cargarCanciones(); } + catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: CancionDto) => { + setAnchorEl(event.currentTarget); setSelectedRow(item); + }; + const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = canciones.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeGestionar && !loadingFiltersDropdown) return {error || "Acceso denegado."}; + + return ( + + Gestionar Canciones + + Filtros + + setFiltroTema(e.target.value)} sx={{minWidth: 200, flexGrow: 1}}/> + setFiltroInterprete(e.target.value)} sx={{minWidth: 200, flexGrow: 1}}/> + + Ritmo + + + + {puedeGestionar && ()} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeGestionar && ( + + + + TemaIntérprete + ÁlbumRitmo + FormatoPista + Acciones + + + {displayData.length === 0 ? ( + No se encontraron canciones. + ) : ( + displayData.map((c) => ( + + {c.tema || '-'} + {c.interprete || '-'} + {c.album || '-'} + {c.nombreRitmo || '-'} + {c.formato || '-'} + {c.pista ?? '-'} + + handleMenuOpen(e, c)} disabled={!puedeGestionar}> + + + )))} + +
+ +
+ )} + + + {puedeGestionar && selectedRow && ( + { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} + {puedeGestionar && selectedRow && ( + handleDelete(selectedRow.id)}> Eliminar)} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarCancionesPage; \ No newline at end of file diff --git a/Frontend/src/pages/Radios/GestionarRitmosPage.tsx b/Frontend/src/pages/Radios/GestionarRitmosPage.tsx new file mode 100644 index 0000000..3fbcd7f --- /dev/null +++ b/Frontend/src/pages/Radios/GestionarRitmosPage.tsx @@ -0,0 +1,164 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, + CircularProgress, Alert +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FilterListIcon from '@mui/icons-material/FilterList'; + +import ritmoService from '../../services/Radios/ritmoService'; +import type { RitmoDto } from '../../models/dtos/Radios/RitmoDto'; +import type { CreateRitmoDto } from '../../models/dtos/Radios/CreateRitmoDto'; +import type { UpdateRitmoDto } from '../../models/dtos/Radios/UpdateRitmoDto'; +import RitmoFormModal from '../../components/Modals/Radios/RitmoFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarRitmosPage: React.FC = () => { + const [ritmos, setRitmos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [filtroNombre, setFiltroNombre] = useState(''); + + const [modalOpen, setModalOpen] = useState(false); + const [editingRitmo, setEditingRitmo] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedRow, setSelectedRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + // Usar el permiso general de la sección Radios (SS005) o crear permisos específicos si es necesario. + const puedeGestionar = isSuperAdmin || tienePermiso("SS005"); + + + const cargarRitmos = useCallback(async () => { + if (!puedeGestionar) { // Asumimos que el mismo permiso es para ver y gestionar + setError("No tiene permiso para acceder a esta sección."); + setLoading(false); + return; + } + setLoading(true); setError(null); setApiErrorMessage(null); + try { + const data = await ritmoService.getAllRitmos(filtroNombre); + setRitmos(data); + } catch (err) { + console.error(err); setError('Error al cargar los ritmos.'); + } finally { setLoading(false); } + }, [puedeGestionar, filtroNombre]); + + useEffect(() => { cargarRitmos(); }, [cargarRitmos]); + + const handleOpenModal = (item?: RitmoDto) => { + setEditingRitmo(item || null); setApiErrorMessage(null); setModalOpen(true); + }; + const handleCloseModal = () => { + setModalOpen(false); setEditingRitmo(null); + }; + + const handleSubmitModal = async (data: CreateRitmoDto | UpdateRitmoDto, id?: number) => { + setApiErrorMessage(null); + try { + if (id && editingRitmo) { + await ritmoService.updateRitmo(id, data as UpdateRitmoDto); + } else { + await ritmoService.createRitmo(data as CreateRitmoDto); + } + cargarRitmos(); + } catch (err: any) { + const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el ritmo.'; + setApiErrorMessage(message); throw err; + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm(`¿Seguro de eliminar este ritmo (ID: ${id})?`)) { + setApiErrorMessage(null); + try { await ritmoService.deleteRitmo(id); cargarRitmos(); } + catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, item: RitmoDto) => { + setAnchorEl(event.currentTarget); setSelectedRow(item); + }; + const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = ritmos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeGestionar) return {error || "Acceso denegado."}; + + return ( + + Gestionar Ritmos + + Filtros + + setFiltroNombre(e.target.value)} sx={{minWidth: 200, flexGrow: 1}}/> + {/* */} + + {puedeGestionar && ()} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && puedeGestionar && ( + + + + Nombre del Ritmo + Acciones + + + {displayData.length === 0 ? ( + No se encontraron ritmos. + ) : ( + displayData.map((r) => ( + + {r.nombreRitmo || '-'} + + handleMenuOpen(e, r)} disabled={!puedeGestionar}> + + + )))} + +
+ +
+ )} + + + {puedeGestionar && selectedRow && ( + { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} + {puedeGestionar && selectedRow && ( + handleDelete(selectedRow.id)}> Eliminar)} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarRitmosPage; \ No newline at end of file diff --git a/Frontend/src/pages/Radios/RadiosIndexPage.tsx b/Frontend/src/pages/Radios/RadiosIndexPage.tsx new file mode 100644 index 0000000..668c71f --- /dev/null +++ b/Frontend/src/pages/Radios/RadiosIndexPage.tsx @@ -0,0 +1,56 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Tabs, Tab, Paper, Typography } from '@mui/material'; +import { Outlet, useNavigate, useLocation } from 'react-router-dom'; + +const radiosSubModules = [ + { label: 'Ritmos', path: 'ritmos' }, + { label: 'Canciones', path: 'canciones' }, + { label: 'Generar Listas', path: 'generar-listas' }, +]; + +const RadiosIndexPage: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const [selectedSubTab, setSelectedSubTab] = useState(false); + + useEffect(() => { + const currentBasePath = '/radios'; + const subPath = location.pathname.startsWith(currentBasePath + '/') + ? location.pathname.substring(currentBasePath.length + 1).split('/')[0] + : (location.pathname === currentBasePath ? radiosSubModules[0]?.path : undefined); + const activeTabIndex = radiosSubModules.findIndex(sm => sm.path === subPath); + + if (activeTabIndex !== -1) { + setSelectedSubTab(activeTabIndex); + } else { + if (location.pathname === currentBasePath && radiosSubModules.length > 0) { + navigate(radiosSubModules[0].path, { replace: true }); + setSelectedSubTab(0); + } else { + setSelectedSubTab(false); + } + } + }, [location.pathname, navigate]); + + const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { + setSelectedSubTab(newValue); + navigate(radiosSubModules[newValue].path); + }; + + return ( + + Módulo de Radios + + + {radiosSubModules.map((subModule) => ( + + ))} + + + + + + + ); +}; +export default RadiosIndexPage; \ No newline at end of file diff --git a/Frontend/src/pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage.tsx b/Frontend/src/pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage.tsx new file mode 100644 index 0000000..4f9a438 --- /dev/null +++ b/Frontend/src/pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage.tsx @@ -0,0 +1,256 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Box, Typography, TextField, Button, Paper, + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, + CircularProgress, Alert, TablePagination, Tooltip, Autocomplete, + MenuItem, + FormControl, + InputLabel, + Select +} from '@mui/material'; +import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto'; +import FilterListIcon from '@mui/icons-material/FilterList'; +import usuarioService from '../../../services/Usuarios/usuarioService'; +import type { UsuarioHistorialDto } from '../../../models/dtos/Usuarios/Auditoria/UsuarioHistorialDto'; +import { usePermissions } from '../../../hooks/usePermissions'; + +const GestionarAuditoriaUsuariosPage: React.FC = () => { + const [historial, setHistorial] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Filtros + const [filtroFechaDesde, setFiltroFechaDesde] = useState(''); + const [filtroFechaHasta, setFiltroFechaHasta] = useState(''); + const [filtroIdUsuarioAfectado, setFiltroIdUsuarioAfectado] = useState(null); + const [filtroIdUsuarioModifico, setFiltroIdUsuarioModifico] = useState(null); + const [filtroTipoMod, setFiltroTipoMod] = useState(''); + + const [usuariosParaDropdown, setUsuariosParaDropdown] = useState([]); + const [tiposModificacionParaDropdown, setTiposModificacionParaDropdown] = useState([]); + const [loadingDropdowns, setLoadingDropdowns] = useState(false); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + const puedeVerAuditoria = isSuperAdmin || tienePermiso("AU001"); // O el permiso que definas + + const fetchDropdownData = useCallback(async () => { + if (!puedeVerAuditoria) return; + setLoadingDropdowns(true); + try { + const usuariosData = await usuarioService.getAllUsuarios(); // Asumiendo que tienes este método + setUsuariosParaDropdown(usuariosData); + + // Opción B para Tipos de Modificación (desde backend) + // const tiposModData = await apiClient.get('/auditoria/tipos-modificacion'); // Ajusta el endpoint si lo creas + // setTiposModificacionParaDropdown(tiposModData.data); + + // Opción A (Hardcodeado en Frontend - más simple para empezar) + setTiposModificacionParaDropdown([ + "Creado", "Insertada", + "Actualizado", "Modificada", + "Eliminado", "Eliminada", + "Baja", "Alta", + "Liquidada", + "Eliminado (Cascada)" + ].sort()); + + } catch (err) { + console.error("Error al cargar datos para dropdowns de auditoría:", err); + setError("Error al cargar opciones de filtro."); // O un error más específico + } finally { + setLoadingDropdowns(false); + } + }, [puedeVerAuditoria]); + + const cargarHistorial = useCallback(async () => { + if (!puedeVerAuditoria) { + setError("No tiene permiso para ver el historial de auditoría."); + setLoading(false); + return; + } + setLoading(true); setError(null); + try { + let data; + // Ahora usamos los IDs de los objetos UsuarioDto seleccionados + const idAfectado = filtroIdUsuarioAfectado ? filtroIdUsuarioAfectado.id : null; + const idModifico = filtroIdUsuarioModifico ? filtroIdUsuarioModifico.id : null; + + if (idAfectado) { // Si se seleccionó un usuario afectado específico + data = await usuarioService.getHistorialDeUsuario(idAfectado, { + fechaDesde: filtroFechaDesde || undefined, + fechaHasta: filtroFechaHasta || undefined, + }); + } else { // Sino, buscar en todo el historial con los otros filtros + data = await usuarioService.getTodoElHistorialDeUsuarios({ + fechaDesde: filtroFechaDesde || undefined, + fechaHasta: filtroFechaHasta || undefined, + idUsuarioModifico: idModifico || undefined, + tipoModificacion: filtroTipoMod || undefined, + }); + } + setHistorial(data); + } catch (err: any) { + console.error(err); + setError(err.response?.data?.message || 'Error al cargar el historial de usuarios.'); + } finally { + setLoading(false); + } + }, [puedeVerAuditoria, filtroFechaDesde, filtroFechaHasta, filtroIdUsuarioAfectado, filtroIdUsuarioModifico, filtroTipoMod]); + + useEffect(() => { + fetchDropdownData(); + }, [fetchDropdownData]); // Cargar al montar + + useEffect(() => { + // Cargar historial cuando los filtros cambian o al inicio si puedeVerAuditoria está listo + if (puedeVerAuditoria) { + cargarHistorial(); + } + }, [cargarHistorial, puedeVerAuditoria]); // Quitar dependencias de filtro directo para evitar llamadas múltiples, handleFiltrar se encarga. + + const handleFiltrar = () => { + setPage(0); + cargarHistorial(); // cargarHistorial ahora usa los estados de filtro directamente + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString('es-AR', { + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', minute: '2-digit', second: '2-digit' + }); + }; + + const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); + }; + const displayData = historial.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeVerAuditoria) { + return {error || "Acceso denegado."}; + } + + + + return ( + + Auditoría de Usuarios + + Filtros + + `${option.nombre} ${option.apellido} (${option.user})`} + value={filtroIdUsuarioAfectado} + onChange={(_event, newValue) => { + setFiltroIdUsuarioAfectado(newValue); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + renderInput={(params) => ( + + )} + loading={loadingDropdowns} + disabled={loadingDropdowns} + sx={{ flexGrow: 1 }} + /> + setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} /> + setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} /> + + `${option.nombre} ${option.apellido} (${option.user})`} + value={filtroIdUsuarioModifico} + onChange={(_event, newValue) => { + setFiltroIdUsuarioModifico(newValue); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + renderInput={(params) => ( + + )} + loading={loadingDropdowns} + disabled={loadingDropdowns} + sx={{ flexGrow: 1 }} + /> + + + Tipo Modificación + + + + + + + + {loading && } + {error && !loading && {error}} + + {!loading && !error && ( + + + + + Fecha + Usuario Afectado (ID) + Username Afectado + Acción + Modificado Por (ID) + Nombre Modificador + Detalles (Simplificado) + + + + {displayData.length === 0 ? ( + No se encontraron registros de historial. + ) : ( + displayData.map((h) => ( + + {formatDate(h.fechaModificacion)} + {h.idUsuarioAfectado} + {h.userAfectado} + {h.tipoModificacion} + {h.idUsuarioModifico} + {h.nombreUsuarioModifico} + + ${h.userNvo}\n` + + `Nombre: ${h.nombreAnt || '-'} -> ${h.nombreNvo}\n` + + `Apellido: ${h.apellidoAnt || '-'} -> ${h.apellidoNvo}\n` + + `Habilitado: ${h.habilitadaAnt ?? '-'} -> ${h.habilitadaNva}\n` + + `SupAdmin: ${h.supAdminAnt ?? '-'} -> ${h.supAdminNvo}\n` + + `Perfil: ${h.nombrePerfilAnt || h.idPerfilAnt || '-'} -> ${h.nombrePerfilNvo} (${h.idPerfilNvo})\n` + + `CambiaClave: ${h.debeCambiarClaveAnt ?? '-'} -> ${h.debeCambiarClaveNva}` + }> + + {`User: ${h.userAnt?.substring(0, 5)}..->${h.userNvo.substring(0, 5)}.., Nom: ${h.nombreAnt?.substring(0, 3)}..->${h.nombreNvo.substring(0, 3)}..`} + + + + + )) + )} + +
+ +
+ )} +
+ ); +}; + +export default GestionarAuditoriaUsuariosPage; \ No newline at end of file diff --git a/Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx b/Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx index 9f8c81e..b155ca0 100644 --- a/Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx +++ b/Frontend/src/pages/Usuarios/UsuariosIndexPage.tsx @@ -6,6 +6,7 @@ const usuariosSubModules = [ { label: 'Perfiles', path: 'perfiles' }, { label: 'Permisos (Definición)', path: 'permisos' }, { label: 'Usuarios', path: 'gestion-usuarios' }, + { label: 'Auditoría Usuarios', path: 'auditoria-usuarios' }, ]; const UsuariosIndexPage: React.FC = () => { diff --git a/Frontend/src/routes/AppRoutes.tsx b/Frontend/src/routes/AppRoutes.tsx index 306a2e9..a26ffa8 100644 --- a/Frontend/src/routes/AppRoutes.tsx +++ b/Frontend/src/routes/AppRoutes.tsx @@ -9,8 +9,6 @@ import { Typography } from '@mui/material'; // Distribución import DistribucionIndexPage from '../pages/Distribucion/DistribucionIndexPage'; -import ESCanillasPage from '../pages/Distribucion/ESCanillasPage'; -import ControlDevolucionesPage from '../pages/Distribucion/ControlDevolucionesPage'; import GestionarCanillitasPage from '../pages/Distribucion/GestionarCanillitasPage'; import GestionarDistribuidoresPage from '../pages/Distribucion/GestionarDistribuidoresPage'; import GestionarPublicacionesPage from '../pages/Distribucion/GestionarPublicacionesPage'; @@ -24,6 +22,8 @@ import GestionarZonasPage from '../pages/Distribucion/GestionarZonasPage'; import GestionarEmpresasPage from '../pages/Distribucion/GestionarEmpresasPage'; import GestionarSalidasOtrosDestinosPage from '../pages/Distribucion/GestionarSalidasOtrosDestinosPage'; import GestionarEntradasSalidasDistPage from '../pages/Distribucion/GestionarEntradasSalidasDistPage'; +import GestionarEntradasSalidasCanillaPage from '../pages/Distribucion/GestionarEntradasSalidasCanillaPage'; +import GestionarControlDevolucionesPage from '../pages/Distribucion/GestionarControlDevolucionesPage'; // Impresión import ImpresionIndexPage from '../pages/Impresion/ImpresionIndexPage'; @@ -36,6 +36,8 @@ import GestionarTiradasPage from '../pages/Impresion/GestionarTiradasPage'; // Contables import ContablesIndexPage from '../pages/Contables/ContablesIndexPage'; import GestionarTiposPagoPage from '../pages/Contables/GestionarTiposPagoPage'; +import GestionarPagosDistribuidorPage from '../pages/Contables/GestionarPagosDistribuidorPage'; +import GestionarNotasCDPage from '../pages/Contables/GestionarNotasCDPage'; // Usuarios import UsuariosIndexPage from '../pages/Usuarios/UsuariosIndexPage'; // Crear este componente @@ -44,6 +46,15 @@ import GestionarPermisosPage from '../pages/Usuarios/GestionarPermisosPage'; import AsignarPermisosAPerfilPage from '../pages/Usuarios/AsignarPermisosAPerfilPage'; import GestionarUsuariosPage from '../pages/Usuarios/GestionarUsuariosPage'; +// Radios +import RadiosIndexPage from '../pages/Radios/RadiosIndexPage'; +import GestionarRitmosPage from '../pages/Radios/GestionarRitmosPage'; +import GestionarCancionesPage from '../pages/Radios/GestionarCancionesPage'; +import GenerarListasRadioPage from '../pages/Radios/GenerarListasRadioPage'; + +// Auditorias +import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage'; + // --- ProtectedRoute y PublicRoute SIN CAMBIOS --- const ProtectedRoute: React.FC<{ children: JSX.Element }> = ({ children }) => { const { isAuthenticated, isLoading } = useAuth(); @@ -101,8 +112,8 @@ const AppRoutes = () => { {/* Módulo de Distribución (anidado) */} }> } /> - } /> - } /> + } /> + } /> } /> } /> } /> @@ -125,7 +136,8 @@ const AppRoutes = () => { }> } /> } /> - {/* Futuras sub-rutas de contables aquí */} + } /> + } /> {/* Módulo de Impresión (anidado) */} @@ -140,7 +152,14 @@ const AppRoutes = () => { {/* Otros Módulos Principales (estos son "finales", no tienen más hijos) */} } /> - } /> + + {/* Módulo de Radios (anidado) */} + }> + } /> + } /> + } /> + } /> + {/* Módulo de Usuarios (anidado) */} }> @@ -149,6 +168,7 @@ const AppRoutes = () => { } /> } /> } /> + } /> {/* Ruta catch-all DENTRO del layout protegido */} diff --git a/Frontend/src/services/Contables/notaCreditoDebitoService.ts b/Frontend/src/services/Contables/notaCreditoDebitoService.ts new file mode 100644 index 0000000..701b060 --- /dev/null +++ b/Frontend/src/services/Contables/notaCreditoDebitoService.ts @@ -0,0 +1,54 @@ +import apiClient from '../apiClient'; +import type { NotaCreditoDebitoDto } from '../../models/dtos/Contables/NotaCreditoDebitoDto'; +import type { CreateNotaDto } from '../../models/dtos/Contables/CreateNotaDto'; +import type { UpdateNotaDto } from '../../models/dtos/Contables/UpdateNotaDto'; + +interface GetAllNotasParams { + fechaDesde?: string | null; // yyyy-MM-dd + fechaHasta?: string | null; // yyyy-MM-dd + destino?: 'Distribuidores' | 'Canillas' | '' | null; + idDestino?: number | null; + idEmpresa?: number | null; + tipoNota?: 'Debito' | 'Credito' | '' | null; +} + +const getAllNotas = async (filters: GetAllNotasParams): Promise => { + const params: Record = {}; + if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde; + if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta; + if (filters.destino) params.destino = filters.destino; + if (filters.idDestino) params.idDestino = filters.idDestino; + if (filters.idEmpresa) params.idEmpresa = filters.idEmpresa; + if (filters.tipoNota) params.tipo = filters.tipoNota; // El backend espera 'tipo' + + const response = await apiClient.get('/notascreditodebito', { params }); + return response.data; +}; + +const getNotaById = async (idNota: number): Promise => { + const response = await apiClient.get(`/notascreditodebito/${idNota}`); + return response.data; +}; + +const createNota = async (data: CreateNotaDto): Promise => { + const response = await apiClient.post('/notascreditodebito', data); + return response.data; +}; + +const updateNota = async (idNota: number, data: UpdateNotaDto): Promise => { + await apiClient.put(`/notascreditodebito/${idNota}`, data); +}; + +const deleteNota = async (idNota: number): Promise => { + await apiClient.delete(`/notascreditodebito/${idNota}`); +}; + +const notaCreditoDebitoService = { + getAllNotas, + getNotaById, + createNota, + updateNota, + deleteNota, +}; + +export default notaCreditoDebitoService; \ No newline at end of file diff --git a/Frontend/src/services/Contables/pagoDistribuidorService.ts b/Frontend/src/services/Contables/pagoDistribuidorService.ts new file mode 100644 index 0000000..c556513 --- /dev/null +++ b/Frontend/src/services/Contables/pagoDistribuidorService.ts @@ -0,0 +1,52 @@ +import apiClient from '../apiClient'; +import type { PagoDistribuidorDto } from '../../models/dtos/Contables/PagoDistribuidorDto'; +import type { CreatePagoDistribuidorDto } from '../../models/dtos/Contables/CreatePagoDistribuidorDto'; +import type { UpdatePagoDistribuidorDto } from '../../models/dtos/Contables/UpdatePagoDistribuidorDto'; + +interface GetAllPagosDistParams { + fechaDesde?: string | null; // yyyy-MM-dd + fechaHasta?: string | null; // yyyy-MM-dd + idDistribuidor?: number | null; + idEmpresa?: number | null; + tipoMovimiento?: 'Recibido' | 'Realizado' | '' | null; +} + +const getAllPagosDistribuidor = async (filters: GetAllPagosDistParams): Promise => { + const params: Record = {}; + if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde; + if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta; + if (filters.idDistribuidor) params.idDistribuidor = filters.idDistribuidor; + if (filters.idEmpresa) params.idEmpresa = filters.idEmpresa; + if (filters.tipoMovimiento) params.tipoMovimiento = filters.tipoMovimiento; + + const response = await apiClient.get('/pagosdistribuidor', { params }); + return response.data; +}; + +const getPagoDistribuidorById = async (idPago: number): Promise => { + const response = await apiClient.get(`/pagosdistribuidor/${idPago}`); + return response.data; +}; + +const createPagoDistribuidor = async (data: CreatePagoDistribuidorDto): Promise => { + const response = await apiClient.post('/pagosdistribuidor', data); + return response.data; +}; + +const updatePagoDistribuidor = async (idPago: number, data: UpdatePagoDistribuidorDto): Promise => { + await apiClient.put(`/pagosdistribuidor/${idPago}`, data); +}; + +const deletePagoDistribuidor = async (idPago: number): Promise => { + await apiClient.delete(`/pagosdistribuidor/${idPago}`); +}; + +const pagoDistribuidorService = { + getAllPagosDistribuidor, + getPagoDistribuidorById, + createPagoDistribuidor, + updatePagoDistribuidor, + deletePagoDistribuidor, +}; + +export default pagoDistribuidorService; \ No newline at end of file diff --git a/Frontend/src/services/Contables/tipoPagoService.ts b/Frontend/src/services/Contables/tipoPagoService.ts index 846d02d..95b0aba 100644 --- a/Frontend/src/services/Contables/tipoPagoService.ts +++ b/Frontend/src/services/Contables/tipoPagoService.ts @@ -1,7 +1,7 @@ import apiClient from '../apiClient'; import type { TipoPago } from '../../models/Entities/TipoPago'; -import type { CreateTipoPagoDto } from '../../models/dtos/tiposPago/CreateTipoPagoDto'; -import type { UpdateTipoPagoDto } from '../../models/dtos/tiposPago/UpdateTipoPagoDto'; +import type { CreateTipoPagoDto } from '../../models/dtos/Contables/CreateTipoPagoDto'; +import type { UpdateTipoPagoDto } from '../../models/dtos/Contables/UpdateTipoPagoDto'; const getAllTiposPago = async (nombreFilter?: string): Promise => { const params: Record = {}; diff --git a/Frontend/src/services/Distribucion/controlDevolucionesService.ts b/Frontend/src/services/Distribucion/controlDevolucionesService.ts new file mode 100644 index 0000000..9b7fc18 --- /dev/null +++ b/Frontend/src/services/Distribucion/controlDevolucionesService.ts @@ -0,0 +1,48 @@ +import apiClient from '../apiClient'; +import type { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto'; +import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto'; +import type { UpdateControlDevolucionesDto } from '../../models/dtos/Distribucion/UpdateControlDevolucionesDto'; + +interface GetAllControlesParams { + fechaDesde?: string | null; // yyyy-MM-dd + fechaHasta?: string | null; // yyyy-MM-dd + idEmpresa?: number | null; +} + +const getAllControlesDevoluciones = async (filters: GetAllControlesParams): Promise => { + const params: Record = {}; + if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde; + if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta; + if (filters.idEmpresa) params.idEmpresa = filters.idEmpresa; + + const response = await apiClient.get('/controldevoluciones', { params }); + return response.data; +}; + +const getControlDevolucionesById = async (idControl: number): Promise => { + const response = await apiClient.get(`/controldevoluciones/${idControl}`); + return response.data; +}; + +const createControlDevoluciones = async (data: CreateControlDevolucionesDto): Promise => { + const response = await apiClient.post('/controldevoluciones', data); + return response.data; +}; + +const updateControlDevoluciones = async (idControl: number, data: UpdateControlDevolucionesDto): Promise => { + await apiClient.put(`/controldevoluciones/${idControl}`, data); +}; + +const deleteControlDevoluciones = async (idControl: number): Promise => { + await apiClient.delete(`/controldevoluciones/${idControl}`); +}; + +const controlDevolucionesService = { + getAllControlesDevoluciones, + getControlDevolucionesById, + createControlDevoluciones, + updateControlDevoluciones, + deleteControlDevoluciones, +}; + +export default controlDevolucionesService; \ No newline at end of file diff --git a/Frontend/src/services/Distribucion/entradaSalidaCanillaService.ts b/Frontend/src/services/Distribucion/entradaSalidaCanillaService.ts new file mode 100644 index 0000000..247e45b --- /dev/null +++ b/Frontend/src/services/Distribucion/entradaSalidaCanillaService.ts @@ -0,0 +1,72 @@ +import apiClient from '../apiClient'; +import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto'; +import type { CreateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaCanillaDto'; // Para creación individual si se mantiene +import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto'; +import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto'; +import type { CreateBulkEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/CreateBulkEntradaSalidaCanillaDto'; + +interface GetAllESCanillaParams { + fechaDesde?: string | null; // yyyy-MM-dd + fechaHasta?: string | null; // yyyy-MM-dd + idPublicacion?: number | null; + idCanilla?: number | null; + liquidados?: boolean | null; // true para solo liquidados, false para solo no liquidados + incluirNoLiquidados?: boolean | null; // Si liquidados es null, este determina si se muestran no liquidados +} + +const getAllEntradasSalidasCanilla = async (filters: GetAllESCanillaParams): Promise => { + const params: Record = {}; + if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde; + if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta; + if (filters.idPublicacion) params.idPublicacion = filters.idPublicacion; + if (filters.idCanilla) params.idCanilla = filters.idCanilla; + if (filters.liquidados !== undefined && filters.liquidados !== null) params.liquidados = filters.liquidados; + if (filters.incluirNoLiquidados !== undefined && filters.incluirNoLiquidados !== null) params.incluirNoLiquidados = filters.incluirNoLiquidados; + + + const response = await apiClient.get('/entradassalidascanilla', { params }); + return response.data; +}; + +const getEntradaSalidaCanillaById = async (idParte: number): Promise => { + const response = await apiClient.get(`/entradassalidascanilla/${idParte}`); + return response.data; +}; + +// Método para creación individual (si decides mantenerlo y diferenciar en el backend o aquí) +const createEntradaSalidaCanilla = async (data: CreateEntradaSalidaCanillaDto): Promise => { + console.warn("Llamando a createEntradaSalidaCanilla (single), considera usar createBulk si es para el modal de múltiples items.") + const response = await apiClient.post('/entradassalidascanilla', data); // Asume que el endpoint /entradassalidascanilla acepta el DTO individual + return response.data; +}; + +// Nuevo método para creación en lote +const createBulkEntradasSalidasCanilla = async (data: CreateBulkEntradaSalidaCanillaDto): Promise => { + const response = await apiClient.post('/entradassalidascanilla/bulk', data); // Endpoint para el lote + return response.data; +}; + + +const updateEntradaSalidaCanilla = async (idParte: number, data: UpdateEntradaSalidaCanillaDto): Promise => { + await apiClient.put(`/entradassalidascanilla/${idParte}`, data); +}; + +const deleteEntradaSalidaCanilla = async (idParte: number): Promise => { + await apiClient.delete(`/entradassalidascanilla/${idParte}`); +}; + +const liquidarMovimientos = async (data: LiquidarMovimientosCanillaRequestDto): Promise => { + await apiClient.post('/entradassalidascanilla/liquidar', data); +}; + +const entradaSalidaCanillaService = { + getAllEntradasSalidasCanilla, + getEntradaSalidaCanillaById, + createEntradaSalidaCanilla, // Mantener si se usa + createBulkEntradasSalidasCanilla, // Nuevo + updateEntradaSalidaCanilla, + deleteEntradaSalidaCanilla, + liquidarMovimientos, +}; + +export default entradaSalidaCanillaService; \ No newline at end of file diff --git a/Frontend/src/services/Distribucion/entradaSalidaDistService.ts b/Frontend/src/services/Distribucion/entradaSalidaDistService.ts index e46753d..522a8fd 100644 --- a/Frontend/src/services/Distribucion/entradaSalidaDistService.ts +++ b/Frontend/src/services/Distribucion/entradaSalidaDistService.ts @@ -12,7 +12,7 @@ interface GetAllESDistParams { } const getAllEntradasSalidasDist = async (filters: GetAllESDistParams): Promise => { - const params: Record = {}; + const params: Record = {}; // Permitir boolean para que coincida con la interfaz if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde; if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta; if (filters.idPublicacion) params.idPublicacion = filters.idPublicacion; diff --git a/Frontend/src/services/Radios/cancionService.ts b/Frontend/src/services/Radios/cancionService.ts new file mode 100644 index 0000000..bfc1ac9 --- /dev/null +++ b/Frontend/src/services/Radios/cancionService.ts @@ -0,0 +1,48 @@ +import apiClient from '../apiClient'; +import type { CancionDto } from '../../models/dtos/Radios/CancionDto'; +import type { CreateCancionDto } from '../../models/dtos/Radios/CreateCancionDto'; +import type { UpdateCancionDto } from '../../models/dtos/Radios/UpdateCancionDto'; + +interface GetAllCancionesParams { + temaFilter?: string | null; + interpreteFilter?: string | null; + idRitmoFilter?: number | null; +} + +const getAllCanciones = async (filters: GetAllCancionesParams): Promise => { + const params: Record = {}; + if (filters.temaFilter) params.tema = filters.temaFilter; // Backend espera 'tema' + if (filters.interpreteFilter) params.interprete = filters.interpreteFilter; // Backend espera 'interprete' + if (filters.idRitmoFilter) params.idRitmo = filters.idRitmoFilter; // Backend espera 'idRitmo' + + const response = await apiClient.get('/canciones', { params }); + return response.data; +}; + +const getCancionById = async (id: number): Promise => { + const response = await apiClient.get(`/canciones/${id}`); + return response.data; +}; + +const createCancion = async (data: CreateCancionDto): Promise => { + const response = await apiClient.post('/canciones', data); + return response.data; +}; + +const updateCancion = async (id: number, data: UpdateCancionDto): Promise => { + await apiClient.put(`/canciones/${id}`, data); +}; + +const deleteCancion = async (id: number): Promise => { + await apiClient.delete(`/canciones/${id}`); +}; + +const cancionService = { + getAllCanciones, + getCancionById, + createCancion, + updateCancion, + deleteCancion, +}; + +export default cancionService; \ No newline at end of file diff --git a/Frontend/src/services/Radios/radioListaService.ts b/Frontend/src/services/Radios/radioListaService.ts new file mode 100644 index 0000000..ff7333a --- /dev/null +++ b/Frontend/src/services/Radios/radioListaService.ts @@ -0,0 +1,20 @@ +import apiClient from '../apiClient'; +import type { GenerarListaRadioRequestDto } from '../../models/dtos/Radios/GenerarListaRadioRequestDto'; +// No esperamos un DTO de respuesta complejo, sino un archivo. + +interface GenerarListaRadioParams extends GenerarListaRadioRequestDto {} + +const generarListaRadio = async (params: GenerarListaRadioParams): Promise => { + // El backend devuelve un archivo (FileContentResult con "application/zip") + // Axios necesita responseType: 'blob' para manejar descargas de archivos. + const response = await apiClient.post('/radios/listas/generar', params, { + responseType: 'blob', + }); + return response.data; // Esto será un Blob +}; + +const radioListaService = { + generarListaRadio, +}; + +export default radioListaService; \ No newline at end of file diff --git a/Frontend/src/services/Radios/ritmoService.ts b/Frontend/src/services/Radios/ritmoService.ts new file mode 100644 index 0000000..3ad66f2 --- /dev/null +++ b/Frontend/src/services/Radios/ritmoService.ts @@ -0,0 +1,40 @@ +import apiClient from '../apiClient'; +import type { RitmoDto } from '../../models/dtos/Radios/RitmoDto'; +import type { CreateRitmoDto } from '../../models/dtos/Radios/CreateRitmoDto'; +import type { UpdateRitmoDto } from '../../models/dtos/Radios/UpdateRitmoDto'; + +const getAllRitmos = async (nombreFilter?: string): Promise => { + const params: Record = {}; + if (nombreFilter) params.nombre = nombreFilter; // El backend espera 'nombre' + + const response = await apiClient.get('/ritmos', { params }); + return response.data; +}; + +const getRitmoById = async (id: number): Promise => { + const response = await apiClient.get(`/ritmos/${id}`); + return response.data; +}; + +const createRitmo = async (data: CreateRitmoDto): Promise => { + const response = await apiClient.post('/ritmos', data); + return response.data; +}; + +const updateRitmo = async (id: number, data: UpdateRitmoDto): Promise => { + await apiClient.put(`/ritmos/${id}`, data); +}; + +const deleteRitmo = async (id: number): Promise => { + await apiClient.delete(`/ritmos/${id}`); +}; + +const ritmoService = { + getAllRitmos, + getRitmoById, + createRitmo, + updateRitmo, + deleteRitmo, +}; + +export default ritmoService; \ No newline at end of file diff --git a/Frontend/src/services/Usuarios/usuarioService.ts b/Frontend/src/services/Usuarios/usuarioService.ts index 0835615..c219530 100644 --- a/Frontend/src/services/Usuarios/usuarioService.ts +++ b/Frontend/src/services/Usuarios/usuarioService.ts @@ -1,9 +1,17 @@ import apiClient from '../apiClient'; +import type { UsuarioHistorialDto } from '../../models/dtos/Usuarios/Auditoria/UsuarioHistorialDto'; import type { UsuarioDto } from '../../models/dtos/Usuarios/UsuarioDto'; import type { CreateUsuarioRequestDto } from '../../models/dtos/Usuarios/CreateUsuarioRequestDto'; import type { UpdateUsuarioRequestDto } from '../../models/dtos/Usuarios/UpdateUsuarioRequestDto'; import type { SetPasswordRequestDto } from '../../models/dtos/Usuarios/SetPasswordRequestDto'; +interface HistorialParams { + fechaDesde?: string | null; // "yyyy-MM-dd" + fechaHasta?: string | null; // "yyyy-MM-dd" + idUsuarioModifico?: number | null; + tipoModificacion?: string | null; +} + const getAllUsuarios = async (userFilter?: string, nombreFilter?: string): Promise => { const params: Record = {}; if (userFilter) params.user = userFilter; @@ -38,6 +46,25 @@ const toggleHabilitado = async (id: number, habilitar: boolean): Promise = }); }; +const getHistorialDeUsuario = async (idUsuarioAfectado: number, params?: Omit): Promise => { + const queryParams: Record = {}; + if (params?.fechaDesde) queryParams.fechaDesde = params.fechaDesde; + if (params?.fechaHasta) queryParams.fechaHasta = params.fechaHasta; + + const response = await apiClient.get(`/usuarios/${idUsuarioAfectado}/historial`, { params: queryParams }); + return response.data; +}; + +const getTodoElHistorialDeUsuarios = async (params?: HistorialParams): Promise => { + const queryParams: Record = {}; + if (params?.fechaDesde) queryParams.fechaDesde = params.fechaDesde; + if (params?.fechaHasta) queryParams.fechaHasta = params.fechaHasta; + if (params?.idUsuarioModifico) queryParams.idUsuarioModifico = params.idUsuarioModifico; + if (params?.tipoModificacion) queryParams.tipoModificacion = params.tipoModificacion; + + const response = await apiClient.get('/usuarios/historial', { params: queryParams }); + return response.data; +}; const usuarioService = { getAllUsuarios, @@ -46,6 +73,8 @@ const usuarioService = { updateUsuario, setPassword, toggleHabilitado, + getHistorialDeUsuario, + getTodoElHistorialDeUsuarios, }; export default usuarioService; \ No newline at end of file