feat: Implementación módulos Empresas, Plantas, Tipos y Estados Bobina

Backend API:
- Implementado CRUD completo para Empresas (DE001-DE004):
  - EmpresaRepository, EmpresaService, EmpresasController.
  - Lógica de creación/eliminación de saldos iniciales en EmpresaService.
  - Transacciones y registro en tablas _H.
  - Verificación de permisos específicos.
- Implementado CRUD completo para Plantas de Impresión (IP001-IP004):
  - PlantaRepository, PlantaService, PlantasController.
  - Transacciones y registro en tablas _H.
  - Verificación de permisos.
- Implementado CRUD completo para Tipos de Bobina (IB006-IB009):
  - TipoBobinaRepository, TipoBobinaService, TiposBobinaController.
  - Transacciones y registro en tablas _H.
  - Verificación de permisos.
- Implementado CRUD completo para Estados de Bobina (IB010-IB013):
  - EstadoBobinaRepository, EstadoBobinaService, EstadosBobinaController.
  - Transacciones y registro en tablas _H.
  - Verificación de permisos.

Frontend React:
- Módulo Empresas:
  - empresaService.ts para interactuar con la API.
  - EmpresaFormModal.tsx para crear/editar empresas.
  - GestionarEmpresasPage.tsx con tabla, filtro, paginación y menú de acciones.
  - Integración con el hook usePermissions para control de acceso.
- Módulo Plantas de Impresión:
  - plantaService.ts.
  - PlantaFormModal.tsx.
  - GestionarPlantasPage.tsx con tabla, filtro, paginación y acciones.
  - Integración con usePermissions.
- Módulo Tipos de Bobina:
  - tipoBobinaService.ts.
  - TipoBobinaFormModal.tsx.
  - GestionarTiposBobinaPage.tsx con tabla, filtro, paginación y acciones.
  - Integración con usePermissions.
- Módulo Estados de Bobina:
  - estadoBobinaService.ts.
  - EstadoBobinaFormModal.tsx.
  - GestionarEstadosBobinaPage.tsx con tabla, filtro, paginación y acciones.
  - Integración con usePermissions.
- Navegación:
  - Añadidas sub-pestañas y rutas para los nuevos módulos dentro de "Distribución" (Empresas) e "Impresión" (Plantas, Tipos Bobina, Estados Bobina).
  - Creado ImpresionIndexPage.tsx para la navegación interna del módulo de Impresión.

Correcciones:
- Corregido el uso de CommitAsync/RollbackAsync a Commit/Rollback síncronos en PlantaService.cs debido a que IDbTransaction no los soporta asíncronamente.
This commit is contained in:
2025-05-09 10:08:53 -03:00
parent 5c4b961073
commit daf84d2708
98 changed files with 6059 additions and 71 deletions

View File

@@ -0,0 +1,19 @@
using System.Threading.Tasks;
using System.Collections.Generic; // Para IEnumerable
using System.Data;
namespace GestionIntegral.Api.Data.Repositories.Contables
{
public interface ISaldoRepository
{
// Necesitaremos un método para obtener los IDs de los distribuidores
Task<IEnumerable<int>> GetAllDistribuidorIdsAsync();
// Método para crear el saldo inicial (podría devolver bool o int)
Task<bool> CreateSaldoInicialAsync(string destino, int idDestino, int idEmpresa, IDbTransaction transaction); // Transacción es clave
// Método para eliminar saldos por IdEmpresa (y opcionalmente por Destino/IdDestino)
Task<bool> DeleteSaldosByEmpresaAsync(int idEmpresa, IDbTransaction transaction); // Transacción es clave
// Método para modificar saldo (lo teníamos como privado antes, ahora en el repo)
Task<bool> ModificarSaldoAsync(string destino, int idDestino, int idEmpresa, decimal montoAAgregar, IDbTransaction? transaction = null);
Task<bool> CheckIfSaldosExistForEmpresaAsync(int id);
}
}

View File

@@ -0,0 +1,17 @@
using GestionIntegral.Api.Models.Contables; // Para TipoPago y TipoPagoHistorico
using System.Collections.Generic;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Data.Repositories.Contables
{
public interface ITipoPagoRepository
{
Task<IEnumerable<TipoPago>> GetAllAsync(string? nombreFilter);
Task<TipoPago?> GetByIdAsync(int id);
Task<TipoPago?> CreateAsync(TipoPago nuevoTipoPago, int idUsuario); // Devuelve el objeto creado o null si falla
Task<bool> UpdateAsync(TipoPago tipoPagoAActualizar, int idUsuario);
Task<bool> DeleteAsync(int id, int idUsuario); // Devuelve true si fue exitoso
Task<bool> ExistsByNameAsync(string nombre, int? excludeId = null);
Task<bool> IsInUseAsync(int id);
}
}

View File

@@ -0,0 +1,134 @@
using Dapper;
using GestionIntegral.Api.Data.Repositories;
using GestionIntegral.Api.Models;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Data.Repositories.Contables
{
public class SaldoRepository : ISaldoRepository
{
private readonly DbConnectionFactory _connectionFactory;
private readonly ILogger<SaldoRepository> _logger;
public SaldoRepository(DbConnectionFactory connectionFactory, ILogger<SaldoRepository> logger)
{
_connectionFactory = connectionFactory;
_logger = logger;
}
public async Task<IEnumerable<int>> GetAllDistribuidorIdsAsync()
{
var sql = "SELECT Id_Distribuidor FROM dbo.dist_dtDistribuidores";
try
{
using (var connection = _connectionFactory.CreateConnection())
{
return await connection.QueryAsync<int>(sql);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener IDs de Distribuidores.");
return Enumerable.Empty<int>();
}
}
public async Task<bool> CreateSaldoInicialAsync(string destino, int idDestino, int idEmpresa, IDbTransaction transaction)
{
var sql = @"
INSERT INTO dbo.cue_Saldos (Destino, Id_Destino, Monto, Id_Empresa)
VALUES (@Destino, @IdDestino, 0.00, @IdEmpresa);";
try
{
int rowsAffected = await transaction.Connection!.ExecuteAsync(sql, // Añadir !
new { Destino = destino, IdDestino = idDestino, IdEmpresa = idEmpresa },
transaction: transaction);
return rowsAffected == 1;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al insertar saldo inicial para {Destino} ID {IdDestino}, Empresa ID {IdEmpresa}.", destino, idDestino, idEmpresa);
throw;
}
}
public async Task<bool> DeleteSaldosByEmpresaAsync(int idEmpresa, IDbTransaction transaction)
{
var sql = "DELETE FROM dbo.cue_Saldos WHERE Id_Empresa = @IdEmpresa";
try
{
await transaction.Connection!.ExecuteAsync(sql, new { IdEmpresa = idEmpresa }, transaction: transaction);
return true; // Asumir éxito si no hay excepción
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al eliminar saldos para Empresa ID {IdEmpresa}.", idEmpresa);
throw;
}
}
public async Task<bool> ModificarSaldoAsync(string destino, int idDestino, int idEmpresa, decimal montoAAgregar, IDbTransaction? transaction = null)
{
var sql = @"UPDATE dbo.cue_Saldos
SET Monto = Monto + @MontoAAgregar
WHERE Destino = @Destino AND Id_Destino = @IdDestino AND Id_Empresa = @IdEmpresa;";
// Usar una variable para la conexión para poder aplicar el '!' si es necesario
IDbConnection connection = transaction?.Connection ?? _connectionFactory.CreateConnection();
bool ownConnection = transaction == null; // Saber si necesitamos cerrar la conexión nosotros
try
{
if (ownConnection) await (connection as System.Data.Common.DbConnection)!.OpenAsync(); // Abrir solo si no hay transacción externa
var parameters = new {
MontoAAgregar = montoAAgregar,
Destino = destino,
IdDestino = idDestino,
IdEmpresa = idEmpresa
};
// Aplicar '!' aquí también si viene de la transacción
int rowsAffected = await connection.ExecuteAsync(sql, parameters, transaction: transaction);
return rowsAffected == 1;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al modificar saldo para {Destino} ID {IdDestino}, Empresa ID {IdEmpresa}.", destino, idDestino, idEmpresa);
if (transaction != null) throw; // Re-lanzar si estamos en una transacción externa
return false; // Devolver false si fue una operación aislada que falló
}
finally
{
// Cerrar la conexión solo si la abrimos nosotros (no había transacción externa)
if (ownConnection && connection.State == ConnectionState.Open)
{
await (connection as System.Data.Common.DbConnection)!.CloseAsync();
}
// Disponer de la conexión si la creamos nosotros
if(ownConnection) (connection as IDisposable)?.Dispose();
}
}
public async Task<bool> CheckIfSaldosExistForEmpresaAsync(int idEmpresa)
{
var sql = "SELECT COUNT(1) FROM dbo.cue_Saldos WHERE Id_Empresa = @IdEmpresa";
try
{
// Este método es de solo lectura, no necesita transacción externa normalmente
using (var connection = _connectionFactory.CreateConnection())
{
var count = await connection.ExecuteScalarAsync<int>(sql, new { IdEmpresa = idEmpresa });
return count > 0; // Devuelve true si hay al menos un saldo
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error en CheckIfSaldosExistForEmpresaAsync para Empresa ID {IdEmpresa}", idEmpresa);
return false; // Asumir que no existen si hay error, para no bloquear la eliminación innecesariamente
// O podrías devolver true para ser más conservador si la verificación es crítica.
}
}
}
}

View File

@@ -0,0 +1,288 @@
using Dapper;
using GestionIntegral.Api.Models.Contables;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Data.Repositories.Contables
{
public class TipoPagoRepository : ITipoPagoRepository
{
private readonly DbConnectionFactory _connectionFactory;
private readonly ILogger<TipoPagoRepository> _logger; // Para logging
public TipoPagoRepository(DbConnectionFactory connectionFactory, ILogger<TipoPagoRepository> logger)
{
_connectionFactory = connectionFactory;
_logger = logger;
}
public async Task<IEnumerable<TipoPago>> GetAllAsync(string? nombreFilter)
{
// Construcción segura de la cláusula WHERE
var sqlBuilder = new System.Text.StringBuilder("SELECT Id_TipoPago AS IdTipoPago, Nombre, Detalle FROM dbo.cue_dtTipopago WHERE 1=1");
var parameters = new DynamicParameters();
if (!string.IsNullOrWhiteSpace(nombreFilter))
{
sqlBuilder.Append(" AND Nombre LIKE @NombreFilter");
parameters.Add("NombreFilter", $"%{nombreFilter}%");
}
sqlBuilder.Append(" ORDER BY Nombre;");
try
{
using (var connection = _connectionFactory.CreateConnection())
{
return await connection.QueryAsync<TipoPago>(sqlBuilder.ToString(), parameters);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener todos los Tipos de Pago. Filtro: {NombreFilter}", nombreFilter);
return Enumerable.Empty<TipoPago>(); // Devolver lista vacía en caso de error
}
}
public async Task<TipoPago?> GetByIdAsync(int id)
{
var sql = "SELECT Id_TipoPago AS IdTipoPago, Nombre, Detalle FROM dbo.cue_dtTipopago WHERE Id_TipoPago = @Id";
try
{
using (var connection = _connectionFactory.CreateConnection())
{
return await connection.QuerySingleOrDefaultAsync<TipoPago>(sql, new { Id = id });
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error al obtener Tipo de Pago por ID: {IdTipoPago}", id);
return null;
}
}
public async Task<bool> ExistsByNameAsync(string nombre, int? excludeId = null)
{
var sqlBuilder = new System.Text.StringBuilder("SELECT COUNT(1) FROM dbo.cue_dtTipopago WHERE Nombre = @Nombre");
var parameters = new DynamicParameters();
parameters.Add("Nombre", nombre);
if (excludeId.HasValue)
{
sqlBuilder.Append(" AND Id_TipoPago != @ExcludeId");
parameters.Add("ExcludeId", excludeId.Value);
}
try
{
using (var connection = _connectionFactory.CreateConnection())
{
var count = await connection.ExecuteScalarAsync<int>(sqlBuilder.ToString(), parameters);
return count > 0;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error en ExistsByNameAsync para Tipo de Pago con nombre: {Nombre}", nombre);
return true; // Asumir que existe para prevenir duplicados si hay error de BD
}
}
public async Task<TipoPago?> CreateAsync(TipoPago nuevoTipoPago, int idUsuario)
{
var sqlInsertPrincipal = @"
INSERT INTO dbo.cue_dtTipopago (Nombre, Detalle)
VALUES (@Nombre, @Detalle);
SELECT CAST(SCOPE_IDENTITY() as int);"; // Obtener el ID generado
var sqlInsertHistorico = @"
INSERT INTO dbo.cue_dtTipopago_H (Id_TipoPago, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod)
VALUES (@IdTipoPago, @Nombre, @Detalle, @IdUsuario, @FechaMod, @TipoMod);";
try
{
using (var connection = _connectionFactory.CreateConnection())
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
// Insertar en la tabla principal y obtener el ID
int nuevoId = await connection.ExecuteScalarAsync<int>(
sqlInsertPrincipal,
new { nuevoTipoPago.Nombre, nuevoTipoPago.Detalle },
transaction: transaction
);
if (nuevoId > 0)
{
nuevoTipoPago.IdTipoPago = nuevoId; // Asignar el ID al objeto
// Insertar en la tabla de historial
await connection.ExecuteAsync(sqlInsertHistorico, new
{
IdTipoPago = nuevoId, // Usar el ID obtenido
nuevoTipoPago.Nombre,
nuevoTipoPago.Detalle,
IdUsuario = idUsuario,
FechaMod = DateTime.Now,
TipoMod = "Insertada"
}, transaction: transaction);
transaction.Commit();
return nuevoTipoPago; // Devolver el objeto completo con ID
}
else
{
transaction.Rollback();
_logger.LogError("SCOPE_IDENTITY() devolvió 0 o menos después de insertar TipoPago. Nombre: {Nombre}", nuevoTipoPago.Nombre);
return null;
}
}
catch (Exception exTrans)
{
transaction.Rollback();
_logger.LogError(exTrans, "Error en transacción CreateAsync para TipoPago. Nombre: {Nombre}", nuevoTipoPago.Nombre);
return null;
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error general en CreateAsync para TipoPago. Nombre: {Nombre}", nuevoTipoPago.Nombre);
return null;
}
}
public async Task<bool> UpdateAsync(TipoPago tipoPagoAActualizar, int idUsuario)
{
// Primero, obtenemos el estado actual para el historial
var tipoPagoActual = await GetByIdAsync(tipoPagoAActualizar.IdTipoPago);
if (tipoPagoActual == null) return false; // No se encontró para actualizar
var sqlUpdate = @"
UPDATE dbo.cue_dtTipopago
SET Nombre = @Nombre, Detalle = @Detalle
WHERE Id_TipoPago = @IdTipoPago;";
var sqlInsertHistorico = @"
INSERT INTO dbo.cue_dtTipopago_H (Id_TipoPago, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod)
VALUES (@IdTipoPago, @NombreActual, @DetalleActual, @IdUsuario, @FechaMod, @TipoMod);";
try
{
using (var connection = _connectionFactory.CreateConnection())
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
// Insertar en historial CON LOS VALORES ANTERIORES
await connection.ExecuteAsync(sqlInsertHistorico, new
{
IdTipoPago = tipoPagoActual.IdTipoPago,
NombreActual = tipoPagoActual.Nombre, // Valor antes del update
DetalleActual = tipoPagoActual.Detalle, // Valor antes del update
IdUsuario = idUsuario,
FechaMod = DateTime.Now,
TipoMod = "Modificada"
}, transaction: transaction);
// Actualizar la tabla principal
var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new
{
tipoPagoAActualizar.Nombre,
tipoPagoAActualizar.Detalle,
tipoPagoAActualizar.IdTipoPago
}, transaction: transaction);
transaction.Commit();
return rowsAffected == 1;
}
catch (Exception exTrans)
{
transaction.Rollback();
_logger.LogError(exTrans, "Error en transacción UpdateAsync para TipoPago ID: {IdTipoPago}", tipoPagoAActualizar.IdTipoPago);
return false;
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error general en UpdateAsync para TipoPago ID: {IdTipoPago}", tipoPagoAActualizar.IdTipoPago);
return false;
}
}
public async Task<bool> DeleteAsync(int id, int idUsuario)
{
var tipoPagoAEliminar = await GetByIdAsync(id);
if (tipoPagoAEliminar == null) return false; // No existe
var sqlDelete = "DELETE FROM dbo.cue_dtTipopago WHERE Id_TipoPago = @Id";
var sqlInsertHistorico = @"
INSERT INTO dbo.cue_dtTipopago_H (Id_TipoPago, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod)
VALUES (@IdTipoPago, @Nombre, @Detalle, @IdUsuario, @FechaMod, @TipoMod);";
try
{
using (var connection = _connectionFactory.CreateConnection())
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
// Insertar en historial ANTES de eliminar
await connection.ExecuteAsync(sqlInsertHistorico, new
{
IdTipoPago = tipoPagoAEliminar.IdTipoPago,
tipoPagoAEliminar.Nombre,
tipoPagoAEliminar.Detalle,
IdUsuario = idUsuario,
FechaMod = DateTime.Now,
TipoMod = "Eliminada"
}, transaction: transaction);
var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction: transaction);
transaction.Commit();
return rowsAffected == 1;
}
catch (Exception exTrans)
{
transaction.Rollback();
_logger.LogError(exTrans, "Error en transacción DeleteAsync para TipoPago ID: {IdTipoPago}", id);
return false;
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error general en DeleteAsync para TipoPago ID: {IdTipoPago}", id);
return false;
}
}
public async Task<bool> IsInUseAsync(int id)
{
var sql = "SELECT COUNT(1) FROM dbo.cue_PagosDistribuidor WHERE Id_TipoPago = @IdTipoPago";
try
{
using (var connection = _connectionFactory.CreateConnection())
{
var count = await connection.ExecuteScalarAsync<int>(sql, new { IdTipoPago = id });
return count > 0;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error en IsInUseAsync para TipoPago ID: {IdTipoPago}", id);
return true; // Asumir que está en uso si hay error para prevenir borrado incorrecto
}
}
}
}