feat: Implementación de Secciones, Recargos, Porc. Pago Dist. y backend E/S Dist.

Backend API:
- Recargos por Zona (`dist_RecargoZona`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/recargos`.
  - Lógica de negocio para vigencias (cierre/reapertura de períodos).
  - Auditoría en `dist_RecargoZona_H`.
- Porcentajes de Pago Distribuidores (`dist_PorcPago`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajespago`.
  - Lógica de negocio para vigencias.
  - Auditoría en `dist_PorcPago_H`.
- Porcentajes/Montos Pago Canillitas (`dist_PorcMonPagoCanilla`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/porcentajesmoncanilla`.
  - Lógica de negocio para vigencias.
  - Auditoría en `dist_PorcMonPagoCanilla_H`.
- Secciones de Publicación (`dist_dtPubliSecciones`):
  - CRUD completo (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Endpoints anidados bajo `/publicaciones/{idPublicacion}/secciones`.
  - Auditoría en `dist_dtPubliSecciones_H`.
- Entradas/Salidas Distribuidores (`dist_EntradasSalidas`):
  - Implementado backend (Modelos, DTOs, Repositorio, Servicio, Controlador).
  - Lógica para determinar precios/recargos/porcentajes aplicables.
  - Cálculo de monto y afectación de saldos de distribuidores en `cue_Saldos`.
  - Auditoría en `dist_EntradasSalidas_H`.
- Correcciones de Mapeo Dapper:
  - Aplicados alias explícitos en repositorios de RecargoZona, PorcPago, PorcMonCanilla, PubliSeccion,
    Canilla, Distribuidor y Precio para asegurar mapeo correcto de IDs y columnas.

Frontend React:
- Recargos por Zona:
  - `recargoZonaService.ts`.
  - `RecargoZonaFormModal.tsx` para crear/editar períodos de recargos.
  - `GestionarRecargosPublicacionPage.tsx` para listar y gestionar recargos por publicación.
- Porcentajes de Pago Distribuidores:
  - `porcPagoService.ts`.
  - `PorcPagoFormModal.tsx`.
  - `GestionarPorcentajesPagoPage.tsx`.
- Porcentajes/Montos Pago Canillitas:
  - `porcMonCanillaService.ts`.
  - `PorcMonCanillaFormModal.tsx`.
  - `GestionarPorcMonCanillaPage.tsx`.
- Secciones de Publicación:
  - `publiSeccionService.ts`.
  - `PubliSeccionFormModal.tsx`.
  - `GestionarSeccionesPublicacionPage.tsx`.
- Navegación:
  - Actualizadas rutas y menús para acceder a la gestión de recargos, porcentajes (dist. y canillita) y secciones desde la vista de una publicación.
- Layout:
  - Uso consistente de `Box` con Flexbox en lugar de `Grid` en nuevos modales y páginas para evitar errores de tipo.
This commit is contained in:
2025-05-21 14:58:52 -03:00
parent b6ba52f074
commit e7e185a9cb
140 changed files with 10465 additions and 394 deletions

View File

@@ -0,0 +1,20 @@
using GestionIntegral.Api.Dtos.Impresion;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Impresion
{
public interface IStockBobinaService
{
Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta);
Task<StockBobinaDto?> ObtenerPorIdAsync(int idBobina);
Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarDatosBobinaDisponibleAsync(int idBobina, UpdateStockBobinaDto updateDto, int idUsuario);
Task<(bool Exito, string? Error)> CambiarEstadoBobinaAsync(int idBobina, CambiarEstadoBobinaDto cambiarEstadoDto, int idUsuario);
Task<(bool Exito, string? Error)> EliminarIngresoErroneoAsync(int idBobina, int idUsuario);
}
}

View File

@@ -0,0 +1,16 @@
using GestionIntegral.Api.Dtos.Impresion;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Impresion
{
public interface ITiradaService
{
Task<IEnumerable<TiradaDto>> ObtenerTiradasAsync(DateTime? fecha, int? idPublicacion, int? idPlanta);
// GetById podría ser útil si se editan tiradas individuales, pero la creación es el foco principal.
// Task<TiradaDto?> ObtenerTiradaPorIdRegistroAsync(int idRegistroTirada); // Para bob_RegTiradas
Task<(TiradaDto? TiradaCreada, string? Error)> RegistrarTiradaCompletaAsync(CreateTiradaRequestDto createDto, int idUsuario);
Task<(bool Exito, string? Error)> EliminarTiradaCompletaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario);
}
}

View File

@@ -0,0 +1,278 @@
using GestionIntegral.Api.Data;
using GestionIntegral.Api.Data.Repositories.Impresion;
using GestionIntegral.Api.Dtos.Impresion;
using GestionIntegral.Api.Models.Impresion;
using GestionIntegral.Api.Models.Distribucion; // Para Publicacion, PubliSeccion
using GestionIntegral.Api.Data.Repositories.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.Impresion
{
public class StockBobinaService : IStockBobinaService
{
private readonly IStockBobinaRepository _stockBobinaRepository;
private readonly ITipoBobinaRepository _tipoBobinaRepository;
private readonly IPlantaRepository _plantaRepository;
private readonly IEstadoBobinaRepository _estadoBobinaRepository;
private readonly IPublicacionRepository _publicacionRepository; // Para validar IdPublicacion
private readonly IPubliSeccionRepository _publiSeccionRepository; // Para validar IdSeccion
private readonly DbConnectionFactory _connectionFactory;
private readonly ILogger<StockBobinaService> _logger;
public StockBobinaService(
IStockBobinaRepository stockBobinaRepository,
ITipoBobinaRepository tipoBobinaRepository,
IPlantaRepository plantaRepository,
IEstadoBobinaRepository estadoBobinaRepository,
IPublicacionRepository publicacionRepository,
IPubliSeccionRepository publiSeccionRepository,
DbConnectionFactory connectionFactory,
ILogger<StockBobinaService> logger)
{
_stockBobinaRepository = stockBobinaRepository;
_tipoBobinaRepository = tipoBobinaRepository;
_plantaRepository = plantaRepository;
_estadoBobinaRepository = estadoBobinaRepository;
_publicacionRepository = publicacionRepository;
_publiSeccionRepository = publiSeccionRepository;
_connectionFactory = connectionFactory;
_logger = logger;
}
// Mapeo complejo porque necesitamos nombres de entidades relacionadas
private async Task<StockBobinaDto> MapToDto(StockBobina bobina)
{
if (bobina == null) return null!; // Debería ser manejado por el llamador
var tipoBobina = await _tipoBobinaRepository.GetByIdAsync(bobina.IdTipoBobina);
var planta = await _plantaRepository.GetByIdAsync(bobina.IdPlanta);
var estado = await _estadoBobinaRepository.GetByIdAsync(bobina.IdEstadoBobina);
Publicacion? publicacion = null;
PubliSeccion? seccion = null;
if (bobina.IdPublicacion.HasValue)
publicacion = await _publicacionRepository.GetByIdSimpleAsync(bobina.IdPublicacion.Value);
if (bobina.IdSeccion.HasValue)
seccion = await _publiSeccionRepository.GetByIdAsync(bobina.IdSeccion.Value); // Asume que GetByIdAsync existe
return new StockBobinaDto
{
IdBobina = bobina.IdBobina,
IdTipoBobina = bobina.IdTipoBobina,
NombreTipoBobina = tipoBobina?.Denominacion ?? "N/A",
NroBobina = bobina.NroBobina,
Peso = bobina.Peso,
IdPlanta = bobina.IdPlanta,
NombrePlanta = planta?.Nombre ?? "N/A",
IdEstadoBobina = bobina.IdEstadoBobina,
NombreEstadoBobina = estado?.Denominacion ?? "N/A",
Remito = bobina.Remito,
FechaRemito = bobina.FechaRemito.ToString("yyyy-MM-dd"),
FechaEstado = bobina.FechaEstado?.ToString("yyyy-MM-dd"),
IdPublicacion = bobina.IdPublicacion,
NombrePublicacion = publicacion?.Nombre,
IdSeccion = bobina.IdSeccion,
NombreSeccion = seccion?.Nombre,
Obs = bobina.Obs
};
}
public async Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta)
{
var bobinas = await _stockBobinaRepository.GetAllAsync(idTipoBobina, nroBobinaFilter, idPlanta, idEstadoBobina, remitoFilter, fechaDesde, fechaHasta);
var dtos = new List<StockBobinaDto>();
foreach (var bobina in bobinas)
{
dtos.Add(await MapToDto(bobina));
}
return dtos;
}
public async Task<StockBobinaDto?> ObtenerPorIdAsync(int idBobina)
{
var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina);
return bobina == null ? null : await MapToDto(bobina);
}
public async Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario)
{
if (await _tipoBobinaRepository.GetByIdAsync(createDto.IdTipoBobina) == null)
return (null, "Tipo de bobina inválido.");
if (await _plantaRepository.GetByIdAsync(createDto.IdPlanta) == null)
return (null, "Planta inválida.");
if (await _stockBobinaRepository.GetByNroBobinaAsync(createDto.NroBobina) != null)
return (null, $"El número de bobina '{createDto.NroBobina}' ya existe.");
var nuevaBobina = new StockBobina
{
IdTipoBobina = createDto.IdTipoBobina,
NroBobina = createDto.NroBobina,
Peso = createDto.Peso,
IdPlanta = createDto.IdPlanta,
IdEstadoBobina = 1, // 1 = Disponible (según contexto previo)
Remito = createDto.Remito,
FechaRemito = createDto.FechaRemito.Date,
FechaEstado = createDto.FechaRemito.Date, // Estado inicial en fecha de remito
IdPublicacion = null,
IdSeccion = null,
Obs = null
};
using var connection = _connectionFactory.CreateConnection();
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
using var transaction = connection.BeginTransaction();
try
{
var bobinaCreada = await _stockBobinaRepository.CreateAsync(nuevaBobina, idUsuario, transaction);
if (bobinaCreada == null) throw new DataException("Error al ingresar la bobina.");
transaction.Commit();
_logger.LogInformation("Bobina ID {Id} ingresada por Usuario ID {UserId}.", bobinaCreada.IdBobina, idUsuario);
return (await MapToDto(bobinaCreada), null);
}
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error IngresarBobinaAsync: {NroBobina}", createDto.NroBobina);
return (null, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> ActualizarDatosBobinaDisponibleAsync(int idBobina, UpdateStockBobinaDto 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 bobinaExistente = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de TX
if (bobinaExistente == null) return (false, "Bobina no encontrada.");
if (bobinaExistente.IdEstadoBobina != 1) // Solo se pueden editar datos si está "Disponible"
return (false, "Solo se pueden modificar los datos de una bobina en estado 'Disponible'.");
// Validar unicidad de NroBobina si cambió
if (bobinaExistente.NroBobina != updateDto.NroBobina &&
await _stockBobinaRepository.GetByNroBobinaAsync(updateDto.NroBobina) != null) // Validar fuera de TX o pasar TX al repo
{
try { transaction.Rollback(); } catch {} // Rollback antes de retornar por validación
return (false, $"El nuevo número de bobina '{updateDto.NroBobina}' ya existe.");
}
if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null)
return (false, "Tipo de bobina inválido.");
if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null)
return (false, "Planta inválida.");
bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina;
bobinaExistente.NroBobina = updateDto.NroBobina;
bobinaExistente.Peso = updateDto.Peso;
bobinaExistente.IdPlanta = updateDto.IdPlanta;
bobinaExistente.Remito = updateDto.Remito;
bobinaExistente.FechaRemito = updateDto.FechaRemito.Date;
// FechaEstado se mantiene ya que el estado no cambia aquí
var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados");
if (!actualizado) throw new DataException("Error al actualizar la bobina.");
transaction.Commit();
return (true, null);
}
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); }
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error ActualizarDatosBobinaDisponibleAsync ID: {IdBobina}", idBobina);
return (false, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> CambiarEstadoBobinaAsync(int idBobina, CambiarEstadoBobinaDto cambiarEstadoDto, 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 bobina = await _stockBobinaRepository.GetByIdAsync(idBobina);
if (bobina == null) return (false, "Bobina no encontrada.");
var nuevoEstado = await _estadoBobinaRepository.GetByIdAsync(cambiarEstadoDto.NuevoEstadoId);
if (nuevoEstado == null) return (false, "El nuevo estado especificado no es válido.");
// Validaciones de flujo de estados
if (bobina.IdEstadoBobina == cambiarEstadoDto.NuevoEstadoId)
return (false, "La bobina ya se encuentra en ese estado.");
if (bobina.IdEstadoBobina == 3 && cambiarEstadoDto.NuevoEstadoId != 3) // 3 = Dañada
return (false, "Una bobina dañada no puede cambiar de estado.");
if (bobina.IdEstadoBobina == 2 && cambiarEstadoDto.NuevoEstadoId == 1) // 2 = En Uso, 1 = Disponible
return (false, "Una bobina 'En Uso' no puede volver a 'Disponible' directamente mediante esta acción.");
bobina.IdEstadoBobina = cambiarEstadoDto.NuevoEstadoId;
bobina.FechaEstado = cambiarEstadoDto.FechaCambioEstado.Date;
bobina.Obs = cambiarEstadoDto.Obs ?? bobina.Obs; // Mantener obs si no se provee uno nuevo
if (cambiarEstadoDto.NuevoEstadoId == 2) // "En Uso"
{
if (!cambiarEstadoDto.IdPublicacion.HasValue || !cambiarEstadoDto.IdSeccion.HasValue)
return (false, "Para el estado 'En Uso', se requiere Publicación y Sección.");
if (await _publicacionRepository.GetByIdSimpleAsync(cambiarEstadoDto.IdPublicacion.Value) == null)
return (false, "Publicación inválida.");
if (await _publiSeccionRepository.GetByIdAsync(cambiarEstadoDto.IdSeccion.Value) == null) // Asume GetByIdAsync en IPubliSeccionRepository
return (false, "Sección inválida.");
bobina.IdPublicacion = cambiarEstadoDto.IdPublicacion.Value;
bobina.IdSeccion = cambiarEstadoDto.IdSeccion.Value;
}
else
{ // Si no es "En Uso", limpiar estos campos
bobina.IdPublicacion = null;
bobina.IdSeccion = null;
}
var actualizado = await _stockBobinaRepository.UpdateAsync(bobina, idUsuario, transaction, $"Estado Cambiado a: {nuevoEstado.Denominacion}");
if (!actualizado) throw new DataException("Error al cambiar estado de la bobina.");
transaction.Commit();
return (true, null);
}
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); }
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error CambiarEstadoBobinaAsync ID: {IdBobina}", idBobina);
return (false, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> EliminarIngresoErroneoAsync(int idBobina, 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 bobina = await _stockBobinaRepository.GetByIdAsync(idBobina);
if (bobina == null) return (false, "Bobina no encontrada.");
if (bobina.IdEstadoBobina != 1) // Solo se pueden eliminar las "Disponibles" (ingresos erróneos)
return (false, "Solo se pueden eliminar ingresos de bobinas que estén en estado 'Disponible'.");
var eliminado = await _stockBobinaRepository.DeleteAsync(idBobina, idUsuario, transaction);
if (!eliminado) throw new DataException("Error al eliminar la bobina.");
transaction.Commit();
return (true, null);
}
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Bobina no encontrada."); }
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error EliminarIngresoErroneoAsync ID: {IdBobina}", idBobina);
return (false, $"Error interno: {ex.Message}");
}
}
}
}

View File

@@ -0,0 +1,224 @@
using GestionIntegral.Api.Data;
using GestionIntegral.Api.Data.Repositories.Distribucion;
using GestionIntegral.Api.Data.Repositories.Impresion;
using GestionIntegral.Api.Dtos.Impresion;
using GestionIntegral.Api.Models.Distribucion; // Para Publicacion, PubliSeccion
using GestionIntegral.Api.Models.Impresion; // Para RegTirada, RegPublicacionSeccion
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Impresion
{
public class TiradaService : ITiradaService
{
private readonly IRegTiradaRepository _regTiradaRepository;
private readonly IRegPublicacionSeccionRepository _regPublicacionSeccionRepository;
private readonly IPublicacionRepository _publicacionRepository;
private readonly IPlantaRepository _plantaRepository;
private readonly IPubliSeccionRepository _publiSeccionRepository; // Para validar IDs de sección
private readonly DbConnectionFactory _connectionFactory;
private readonly ILogger<TiradaService> _logger;
public TiradaService(
IRegTiradaRepository regTiradaRepository,
IRegPublicacionSeccionRepository regPublicacionSeccionRepository,
IPublicacionRepository publicacionRepository,
IPlantaRepository plantaRepository,
IPubliSeccionRepository publiSeccionRepository,
DbConnectionFactory connectionFactory,
ILogger<TiradaService> logger)
{
_regTiradaRepository = regTiradaRepository;
_regPublicacionSeccionRepository = regPublicacionSeccionRepository;
_publicacionRepository = publicacionRepository;
_plantaRepository = plantaRepository;
_publiSeccionRepository = publiSeccionRepository;
_connectionFactory = connectionFactory;
_logger = logger;
}
public async Task<IEnumerable<TiradaDto>> ObtenerTiradasAsync(DateTime? fecha, int? idPublicacion, int? idPlanta)
{
var tiradasPrincipales = await _regTiradaRepository.GetByCriteriaAsync(fecha, idPublicacion, idPlanta);
var resultado = new List<TiradaDto>();
foreach (var tiradaP in tiradasPrincipales)
{
var publicacion = await _publicacionRepository.GetByIdSimpleAsync(tiradaP.IdPublicacion);
var planta = await _plantaRepository.GetByIdAsync(tiradaP.IdPlanta);
var seccionesImpresas = await _regPublicacionSeccionRepository.GetByFechaPublicacionPlantaAsync(tiradaP.Fecha, tiradaP.IdPublicacion, tiradaP.IdPlanta);
var detallesSeccionDto = new List<DetalleSeccionEnListadoDto>();
int totalPaginas = 0;
foreach (var seccionImp in seccionesImpresas)
{
var seccionInfo = await _publiSeccionRepository.GetByIdAsync(seccionImp.IdSeccion);
detallesSeccionDto.Add(new DetalleSeccionEnListadoDto
{
IdSeccion = seccionImp.IdSeccion,
NombreSeccion = seccionInfo?.Nombre ?? "Sección Desconocida",
CantPag = seccionImp.CantPag,
IdRegPublicacionSeccion = seccionImp.IdTirada // Este es el PK de bob_RegPublicaciones
});
totalPaginas += seccionImp.CantPag;
}
resultado.Add(new TiradaDto
{
IdRegistroTirada = tiradaP.IdRegistro,
IdPublicacion = tiradaP.IdPublicacion,
NombrePublicacion = publicacion?.Nombre ?? "Publicación Desconocida",
Fecha = tiradaP.Fecha.ToString("yyyy-MM-dd"),
IdPlanta = tiradaP.IdPlanta,
NombrePlanta = planta?.Nombre ?? "Planta Desconocida",
Ejemplares = tiradaP.Ejemplares,
SeccionesImpresas = detallesSeccionDto,
TotalPaginasSumadas = totalPaginas
});
}
return resultado;
}
public async Task<(TiradaDto? TiradaCreada, string? Error)> RegistrarTiradaCompletaAsync(CreateTiradaRequestDto createDto, int idUsuario)
{
// Validaciones previas
var publicacion = await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion);
if (publicacion == null) return (null, "La publicación especificada no existe.");
var planta = await _plantaRepository.GetByIdAsync(createDto.IdPlanta);
if (planta == null) return (null, "La planta especificada no existe.");
// Validar que no exista ya una tirada para esa Publicación, Fecha y Planta
// (bob_RegTiradas debería ser único por estos campos)
if (await _regTiradaRepository.GetByFechaPublicacionPlantaAsync(createDto.Fecha.Date, createDto.IdPublicacion, createDto.IdPlanta) != null)
{
return (null, $"Ya existe una tirada registrada para la publicación '{publicacion.Nombre}' en la planta '{planta.Nombre}' para la fecha {createDto.Fecha:dd/MM/yyyy}.");
}
// Validar secciones
foreach (var seccionDto in createDto.Secciones)
{
var seccionDb = await _publiSeccionRepository.GetByIdAsync(seccionDto.IdSeccion);
if (seccionDb == null || seccionDb.IdPublicacion != createDto.IdPublicacion)
return (null, $"La sección con ID {seccionDto.IdSeccion} no es válida o no pertenece a la publicación seleccionada.");
if (!seccionDb.Estado) // Asumiendo que solo se pueden tirar secciones activas
return (null, $"La sección '{seccionDb.Nombre}' no está activa y no puede incluirse en la tirada.");
}
using var connection = _connectionFactory.CreateConnection();
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
using var transaction = connection.BeginTransaction();
try
{
// 1. Crear registro en bob_RegTiradas (total de ejemplares)
var nuevaRegTirada = new RegTirada
{
IdPublicacion = createDto.IdPublicacion,
Fecha = createDto.Fecha.Date,
IdPlanta = createDto.IdPlanta,
Ejemplares = createDto.Ejemplares
};
var regTiradaCreada = await _regTiradaRepository.CreateAsync(nuevaRegTirada, idUsuario, transaction);
if (regTiradaCreada == null) throw new DataException("Error al registrar el total de la tirada.");
// 2. Crear registros en bob_RegPublicaciones (detalle de secciones y páginas)
var seccionesImpresasDto = new List<DetalleSeccionEnListadoDto>();
int totalPaginasSumadas = 0;
foreach (var seccionDto in createDto.Secciones)
{
var nuevaRegPubSeccion = new RegPublicacionSeccion
{
IdPublicacion = createDto.IdPublicacion,
IdSeccion = seccionDto.IdSeccion,
CantPag = seccionDto.CantPag,
Fecha = createDto.Fecha.Date,
IdPlanta = createDto.IdPlanta
};
var seccionCreadaEnTirada = await _regPublicacionSeccionRepository.CreateAsync(nuevaRegPubSeccion, idUsuario, transaction);
if (seccionCreadaEnTirada == null) throw new DataException($"Error al registrar la sección ID {seccionDto.IdSeccion} en la tirada.");
var seccionInfo = await _publiSeccionRepository.GetByIdAsync(seccionDto.IdSeccion); // Para obtener nombre
seccionesImpresasDto.Add(new DetalleSeccionEnListadoDto{
IdSeccion = seccionCreadaEnTirada.IdSeccion,
NombreSeccion = seccionInfo?.Nombre ?? "N/A",
CantPag = seccionCreadaEnTirada.CantPag,
IdRegPublicacionSeccion = seccionCreadaEnTirada.IdTirada
});
totalPaginasSumadas += seccionCreadaEnTirada.CantPag;
}
transaction.Commit();
_logger.LogInformation("Tirada completa registrada para Pub ID {IdPub}, Fecha {Fecha}, Planta ID {IdPlanta} por Usuario ID {UserId}.",
createDto.IdPublicacion, createDto.Fecha.Date, createDto.IdPlanta, idUsuario);
return (new TiradaDto {
IdRegistroTirada = regTiradaCreada.IdRegistro,
IdPublicacion = regTiradaCreada.IdPublicacion,
NombrePublicacion = publicacion.Nombre,
Fecha = regTiradaCreada.Fecha.ToString("yyyy-MM-dd"),
IdPlanta = regTiradaCreada.IdPlanta,
NombrePlanta = planta.Nombre,
Ejemplares = regTiradaCreada.Ejemplares,
SeccionesImpresas = seccionesImpresasDto,
TotalPaginasSumadas = totalPaginasSumadas
}, null);
}
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error RegistrarTiradaCompletaAsync para Pub ID {IdPub}, Fecha {Fecha}", createDto.IdPublicacion, createDto.Fecha);
return (null, $"Error interno: {ex.Message}");
}
}
public async Task<(bool Exito, string? Error)> EliminarTiradaCompletaAsync(DateTime fecha, int idPublicacion, int idPlanta, int idUsuario)
{
// Verificar que la tirada principal exista
var tiradaPrincipal = await _regTiradaRepository.GetByFechaPublicacionPlantaAsync(fecha.Date, idPublicacion, idPlanta);
if (tiradaPrincipal == null)
{
return (false, "No se encontró una tirada principal para los criterios especificados.");
}
using var connection = _connectionFactory.CreateConnection();
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
using var transaction = connection.BeginTransaction();
try
{
// 1. Eliminar detalles de secciones de bob_RegPublicaciones
// El método ya guarda en historial
await _regPublicacionSeccionRepository.DeleteByFechaPublicacionPlantaAsync(fecha.Date, idPublicacion, idPlanta, idUsuario, transaction);
_logger.LogInformation("Secciones de tirada eliminadas para Fecha: {Fecha}, PubID: {IdPublicacion}, PlantaID: {IdPlanta}", fecha.Date, idPublicacion, idPlanta);
// 2. Eliminar el registro principal de bob_RegTiradas
// El método ya guarda en historial
bool principalEliminado = await _regTiradaRepository.DeleteByFechaPublicacionPlantaAsync(fecha.Date, idPublicacion, idPlanta, idUsuario, transaction);
// bool principalEliminado = await _regTiradaRepository.DeleteAsync(tiradaPrincipal.IdRegistro, idUsuario, transaction); // Alternativa si ya tienes el IdRegistro
if (!principalEliminado && tiradaPrincipal != null) // Si DeleteByFechaPublicacionPlantaAsync devuelve false porque no encontró nada (raro si pasó la validación)
{
_logger.LogWarning("No se eliminó el registro principal de tirada (bob_RegTiradas) para Fecha: {Fecha}, PubID: {IdPublicacion}, PlantaID: {IdPlanta}. Pudo haber sido eliminado concurrentemente.", fecha.Date, idPublicacion, idPlanta);
// Decidir si esto es un error que debe hacer rollback
}
_logger.LogInformation("Registro principal de tirada eliminado para Fecha: {Fecha}, PubID: {IdPublicacion}, PlantaID: {IdPlanta}", fecha.Date, idPublicacion, idPlanta);
transaction.Commit();
return (true, null);
}
catch (Exception ex)
{
try { transaction.Rollback(); } catch { }
_logger.LogError(ex, "Error EliminarTiradaCompletaAsync para Fecha {Fecha}, PubID {IdPub}, PlantaID {IdPlanta}", fecha.Date, idPublicacion, idPlanta);
return (false, $"Error interno al eliminar la tirada: {ex.Message}");
}
}
}
}