diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/EmpresasController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/EmpresasController.cs new file mode 100644 index 0000000..0c3de6b --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/EmpresasController.cs @@ -0,0 +1,217 @@ +// src/Controllers/EmpresasController.cs + +using GestionIntegral.Api.Dtos.Empresas; // Para los DTOs (EmpresaDto, CreateEmpresaDto, UpdateEmpresaDto) +using GestionIntegral.Api.Services.Distribucion; // Para IEmpresaService +using Microsoft.AspNetCore.Authorization; // Para [Authorize] +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; // Para obtener el IdUsuario del token +using Microsoft.Extensions.Logging; // Para ILogger + +namespace GestionIntegral.Api.Controllers // Ajusta el namespace si es necesario +{ + [Route("api/[controller]")] // Ruta base: /api/empresas + [ApiController] + [Authorize] // Proteger todos los endpoints por defecto, requiere autenticación + public class EmpresasController : ControllerBase + { + private readonly IEmpresaService _empresaService; + private readonly ILogger _logger; + + public EmpresasController(IEmpresaService empresaService, ILogger logger) + { + _empresaService = empresaService ?? throw new ArgumentNullException(nameof(empresaService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + // --- Helper para verificar permisos --- + // (Similar al de ZonasController) + private bool TienePermiso(string codAccRequerido) + { + // SuperAdmin siempre tiene permiso + if (User.IsInRole("SuperAdmin")) return true; + // Busca si el claim 'permission' con el valor requerido existe + return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido); + } + + // --- Helper para obtener User ID --- + // (Similar al de otros controladores) + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) + { + return userId; + } + _logger.LogWarning("No se pudo obtener el UserId del token JWT en EmpresasController."); + return null; + } + + // --- Endpoints CRUD --- + + // GET: api/empresas + // Permiso Requerido: DE001 (Ver Empresas) + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetAllEmpresas([FromQuery] string? nombre, [FromQuery] string? detalle) + { + if (!TienePermiso("DE001")) + { + _logger.LogWarning("Acceso denegado a GetAllEmpresas para el usuario {UserId}", GetCurrentUserId() ?? 0); + return Forbid(); // 403 Forbidden + } + + try + { + var empresas = await _empresaService.ObtenerTodasAsync(nombre, detalle); + return Ok(empresas); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todas las Empresas. Filtros: Nombre={Nombre}, Detalle={Detalle}", nombre, detalle); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener las empresas."); + } + } + + // GET: api/empresas/{id} + // Permiso Requerido: DE001 (Ver Empresas) + [HttpGet("{id:int}", Name = "GetEmpresaById")] + [ProducesResponseType(typeof(EmpresaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetEmpresaById(int id) + { + if (!TienePermiso("DE001")) return Forbid(); + + try + { + var empresa = await _empresaService.ObtenerPorIdAsync(id); + if (empresa == null) + { + return NotFound(new { message = $"Empresa con ID {id} no encontrada." }); + } + return Ok(empresa); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Empresa por ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener la empresa."); + } + } + + // POST: api/empresas + // Permiso Requerido: DE002 (Agregar Empresas) + [HttpPost] + [ProducesResponseType(typeof(EmpresaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] // Por validación DTO o de negocio + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreateEmpresa([FromBody] CreateEmpresaDto createDto) + { + if (!TienePermiso("DE002")) return Forbid(); + + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (empresaCreada, error) = await _empresaService.CrearAsync(createDto, idUsuario.Value); + + if (error != null) + { + // Error de lógica de negocio (ej: nombre duplicado) + return BadRequest(new { message = error }); + } + if (empresaCreada == null) // Fallo inesperado en el servicio + { + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al crear la empresa."); + } + + // Devuelve 201 Created con la ubicación del nuevo recurso y el recurso mismo + return CreatedAtRoute("GetEmpresaById", new { id = empresaCreada.IdEmpresa }, empresaCreada); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear Empresa. Nombre: {Nombre} por Usuario ID: {UsuarioId}", createDto.Nombre, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la creación de la empresa."); + } + } + + // PUT: api/empresas/{id} + // Permiso Requerido: DE003 (Editar Empresas) + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] // Éxito sin contenido + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task UpdateEmpresa(int id, [FromBody] UpdateEmpresaDto updateDto) + { + if (!TienePermiso("DE003")) return Forbid(); + + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (exito, error) = await _empresaService.ActualizarAsync(id, updateDto, idUsuario.Value); + + if (!exito) + { + if (error == "Empresa no encontrada.") return NotFound(new { message = error }); + // Otro error de lógica de negocio (ej: nombre duplicado) + return BadRequest(new { message = error }); + } + return NoContent(); // Éxito + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar Empresa ID: {Id} por Usuario ID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al actualizar la empresa."); + } + } + + // DELETE: api/empresas/{id} + // Permiso Requerido: DE004 (Eliminar Empresas) + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] // Si está en uso + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task DeleteEmpresa(int id) + { + if (!TienePermiso("DE004")) return Forbid(); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (exito, error) = await _empresaService.EliminarAsync(id, idUsuario.Value); + + if (!exito) + { + if (error == "Empresa no encontrada.") return NotFound(new { message = error }); + // Otro error de lógica de negocio (ej: "En uso") + return BadRequest(new { message = error }); + } + return NoContent(); // Éxito + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar Empresa ID: {Id} por Usuario ID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al eliminar la empresa."); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Distribucion/ZonasController.cs b/Backend/GestionIntegral.Api/Controllers/Distribucion/ZonasController.cs new file mode 100644 index 0000000..44bf858 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Distribucion/ZonasController.cs @@ -0,0 +1,190 @@ +using GestionIntegral.Api.Dtos.Zonas; // Para los DTOs +using GestionIntegral.Api.Services.Distribucion; // Para IZonaService +using Microsoft.AspNetCore.Authorization; // Para [Authorize] +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace GestionIntegral.Api.Controllers.Distribucion +{ + [Route("api/[controller]")] + [ApiController] + [Authorize] // <-- Protección GENERAL: El usuario debe estar logueado + public class ZonasController : ControllerBase + { + private readonly IZonaService _zonaService; + private readonly ILogger _logger; + + public ZonasController(IZonaService zonaService, ILogger logger) + { + _zonaService = zonaService; + _logger = logger; + } + + // Helper para verificar permisos directamente desde los claims del token + private bool TienePermiso(string codAccRequerido) + { + // SuperAdmin siempre tiene permiso + if (User.IsInRole("SuperAdmin")) return true; + // Busca si el claim 'permission' con el valor requerido existe + return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido); + } + + // Helper para obtener el ID del usuario del token + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) + { + return userId; + } + _logger.LogWarning("No se pudo obtener el UserId del token JWT en ZonasController."); + return null; + } + + + // GET: api/zonas + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task GetAllZonas([FromQuery] string? nombre, [FromQuery] string? descripcion) + { + // Verificar permiso ZD001 + if (!TienePermiso("ZD001")) + { + _logger.LogWarning("Acceso denegado a GetAllZonas para el usuario {UserId}", GetCurrentUserId() ?? 0); + return Forbid(); // Devolver 403 Forbidden + } + + try + { + var zonas = await _zonaService.ObtenerTodasAsync(nombre, descripcion); + return Ok(zonas); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todas las Zonas."); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + + // GET: api/zonas/{id} + [HttpGet("{id:int}", Name = "GetZonaById")] + [ProducesResponseType(typeof(ZonaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetZonaById(int id) + { + if (!TienePermiso("ZD001")) return Forbid(); + + try + { + var zona = await _zonaService.ObtenerPorIdAsync(id); + if (zona == null) + { + return NotFound(new { message = $"Zona con ID {id} no encontrada o inactiva." }); + } + return Ok(zona); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Zona por ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + + // POST: api/zonas + [HttpPost] + [ProducesResponseType(typeof(ZonaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task CreateZona([FromBody] CreateZonaDto createDto) + { + if (!TienePermiso("ZD002")) return Forbid(); + + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (zonaCreada, error) = await _zonaService.CrearAsync(createDto, idUsuario.Value); + + if (error != null) return BadRequest(new { message = error }); + if (zonaCreada == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear la zona."); + + return CreatedAtRoute("GetZonaById", new { id = zonaCreada.IdZona }, zonaCreada); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear Zona. Nombre: {Nombre}", createDto.Nombre); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + + // PUT: api/zonas/{id} + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateZona(int id, [FromBody] UpdateZonaDto updateDto) + { + if (!TienePermiso("ZD003")) return Forbid(); + + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (exito, error) = await _zonaService.ActualizarAsync(id, updateDto, idUsuario.Value); + + if (!exito) + { + if (error != null && (error.Contains("no encontrada") || error.Contains("eliminada (inactiva)"))) + return NotFound(new { message = error }); + return BadRequest(new { message = error ?? "Error desconocido al actualizar." }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar Zona ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + + // DELETE: api/zonas/{id} + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] // Si está en uso + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteZona(int id) + { + if (!TienePermiso("ZD004")) return Forbid(); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (exito, error) = await _zonaService.EliminarAsync(id, idUsuario.Value); + + if (!exito) + { + if (error == "Zona no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error ?? "Error desconocido al eliminar." }); // Ej: "En uso" + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar Zona ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al procesar la solicitud."); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Impresion/EstadosBobinaController.cs b/Backend/GestionIntegral.Api/Controllers/Impresion/EstadosBobinaController.cs new file mode 100644 index 0000000..186b6aa --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Impresion/EstadosBobinaController.cs @@ -0,0 +1,185 @@ +using GestionIntegral.Api.Dtos.Impresion; +using GestionIntegral.Api.Services.Impresion; +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.Impresion +{ + [Route("api/[controller]")] // Ruta base: /api/estadosbobina + [ApiController] + [Authorize] + public class EstadosBobinaController : ControllerBase + { + private readonly IEstadoBobinaService _estadoBobinaService; + private readonly ILogger _logger; + + // Asumiendo códigos de permiso para Estados de Bobina + private const string PermisoVer = "IB010"; + private const string PermisoCrear = "IB011"; + private const string PermisoModificar = "IB012"; + private const string PermisoEliminar = "IB013"; + + public EstadosBobinaController(IEstadoBobinaService estadoBobinaService, ILogger logger) + { + _estadoBobinaService = estadoBobinaService ?? throw new ArgumentNullException(nameof(estadoBobinaService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + // --- Helper para permisos --- + private bool TienePermiso(string codAccRequerido) + { + if (User.IsInRole("SuperAdmin")) return true; + return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido); + } + + // --- Helper para User ID --- + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en EstadosBobinaController."); + return null; + } + + // GET: api/estadosbobina + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetAllEstadosBobina([FromQuery] string? denominacion) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + + try + { + var estados = await _estadoBobinaService.ObtenerTodosAsync(denominacion); + return Ok(estados); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Estados de Bobina. Filtro: {Denominacion}", denominacion); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener los estados de bobina."); + } + } + + // GET: api/estadosbobina/{id} + [HttpGet("{id:int}", Name = "GetEstadoBobinaById")] + [ProducesResponseType(typeof(EstadoBobinaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetEstadoBobinaById(int id) + { + if (!TienePermiso(PermisoVer)) return Forbid(); + + try + { + var estado = await _estadoBobinaService.ObtenerPorIdAsync(id); + if (estado == null) return NotFound(new { message = $"Estado de bobina con ID {id} no encontrado." }); + return Ok(estado); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Estado de Bobina por ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener el estado de bobina."); + } + } + + // POST: api/estadosbobina + [HttpPost] + [ProducesResponseType(typeof(EstadoBobinaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreateEstadoBobina([FromBody] CreateEstadoBobinaDto createDto) + { + if (!TienePermiso(PermisoCrear)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (estadoCreado, error) = await _estadoBobinaService.CrearAsync(createDto, idUsuario.Value); + if (error != null) return BadRequest(new { message = error }); + if (estadoCreado == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear el estado de bobina."); + return CreatedAtRoute("GetEstadoBobinaById", new { id = estadoCreado.IdEstadoBobina }, estadoCreado); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear Estado de Bobina. Denominación: {Denominacion} por Usuario ID: {UsuarioId}", createDto.Denominacion, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al crear el estado de bobina."); + } + } + + // PUT: api/estadosbobina/{id} + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task UpdateEstadoBobina(int id, [FromBody] UpdateEstadoBobinaDto updateDto) + { + if (!TienePermiso(PermisoModificar)) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (exito, error) = await _estadoBobinaService.ActualizarAsync(id, updateDto, idUsuario.Value); + if (!exito) + { + if (error == "Estado de bobina no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar Estado de Bobina ID: {Id} por Usuario ID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al actualizar el estado de bobina."); + } + } + + // DELETE: api/estadosbobina/{id} + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] // Si está en uso o es estado base + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task DeleteEstadoBobina(int id) + { + if (!TienePermiso(PermisoEliminar)) return Forbid(); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (exito, error) = await _estadoBobinaService.EliminarAsync(id, idUsuario.Value); + if (!exito) + { + if (error == "Estado de bobina no encontrado.") return NotFound(new { message = error }); + // Otros errores (en uso, estado base) + return BadRequest(new { message = error }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar Estado de Bobina ID: {Id} por Usuario ID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al eliminar el estado de bobina."); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Impresion/PlantasController.cs b/Backend/GestionIntegral.Api/Controllers/Impresion/PlantasController.cs new file mode 100644 index 0000000..b2941d4 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Impresion/PlantasController.cs @@ -0,0 +1,204 @@ +using GestionIntegral.Api.Dtos.Impresion; // Para los DTOs +using GestionIntegral.Api.Services.Impresion; // Para IPlantaService +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.Impresion +{ + [Route("api/[controller]")] // Ruta base: /api/plantas + [ApiController] + [Authorize] // Requiere autenticación para todos los endpoints + public class PlantasController : ControllerBase + { + private readonly IPlantaService _plantaService; + private readonly ILogger _logger; + + public PlantasController(IPlantaService plantaService, ILogger logger) + { + _plantaService = plantaService ?? throw new ArgumentNullException(nameof(plantaService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + // --- Helper para verificar permisos --- + private bool TienePermiso(string codAccRequerido) + { + if (User.IsInRole("SuperAdmin")) return true; + return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido); + } + + // --- Helper para obtener User ID --- + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) + { + return userId; + } + _logger.LogWarning("No se pudo obtener el UserId del token JWT en PlantasController."); + return null; + } + + // --- Endpoints CRUD --- + + // GET: api/plantas + // Permiso: IP001 (Ver Plantas) + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetAllPlantas([FromQuery] string? nombre, [FromQuery] string? detalle) + { + if (!TienePermiso("IP001")) + { + _logger.LogWarning("Acceso denegado a GetAllPlantas para el usuario {UserId}", GetCurrentUserId() ?? 0); + return Forbid(); + } + + try + { + var plantas = await _plantaService.ObtenerTodasAsync(nombre, detalle); + return Ok(plantas); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todas las Plantas. Filtros: Nombre={Nombre}, Detalle={Detalle}", nombre, detalle); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener las plantas."); + } + } + + // GET: api/plantas/{id} + // Permiso: IP001 (Ver Plantas) + [HttpGet("{id:int}", Name = "GetPlantaById")] + [ProducesResponseType(typeof(PlantaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetPlantaById(int id) + { + if (!TienePermiso("IP001")) return Forbid(); + + try + { + var planta = await _plantaService.ObtenerPorIdAsync(id); + if (planta == null) + { + return NotFound(new { message = $"Planta con ID {id} no encontrada." }); + } + return Ok(planta); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Planta por ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener la planta."); + } + } + + // POST: api/plantas + // Permiso: IP002 (Agregar Plantas) + [HttpPost] + [ProducesResponseType(typeof(PlantaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreatePlanta([FromBody] CreatePlantaDto createDto) + { + if (!TienePermiso("IP002")) return Forbid(); + + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (plantaCreada, error) = await _plantaService.CrearAsync(createDto, idUsuario.Value); + + if (error != null) return BadRequest(new { message = error }); + if (plantaCreada == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear la planta."); + + return CreatedAtRoute("GetPlantaById", new { id = plantaCreada.IdPlanta }, plantaCreada); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear Planta. Nombre: {Nombre} por Usuario ID: {UsuarioId}", createDto.Nombre, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al crear la planta."); + } + } + + // PUT: api/plantas/{id} + // Permiso: IP003 (Modificar Plantas) + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task UpdatePlanta(int id, [FromBody] UpdatePlantaDto updateDto) + { + if (!TienePermiso("IP003")) return Forbid(); + + if (!ModelState.IsValid) return BadRequest(ModelState); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (exito, error) = await _plantaService.ActualizarAsync(id, updateDto, idUsuario.Value); + + if (!exito) + { + if (error == "Planta no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar Planta ID: {Id} por Usuario ID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al actualizar la planta."); + } + } + + // DELETE: api/plantas/{id} + // Permiso: IP004 (Eliminar Plantas) + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] // Si está en uso + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task DeletePlanta(int id) + { + if (!TienePermiso("IP004")) return Forbid(); + + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("No se pudo obtener el ID del usuario del token."); + + try + { + var (exito, error) = await _plantaService.EliminarAsync(id, idUsuario.Value); + + if (!exito) + { + if (error == "Planta no encontrada.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); // Ej: "En uso" + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar Planta ID: {Id} por Usuario ID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al eliminar la planta."); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Controllers/Impresion/TiposBobinaController.cs b/Backend/GestionIntegral.Api/Controllers/Impresion/TiposBobinaController.cs new file mode 100644 index 0000000..67670e2 --- /dev/null +++ b/Backend/GestionIntegral.Api/Controllers/Impresion/TiposBobinaController.cs @@ -0,0 +1,183 @@ +using GestionIntegral.Api.Dtos.Impresion; +using GestionIntegral.Api.Services.Impresion; +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.Impresion +{ + [Route("api/[controller]")] // Ruta base: /api/tiposbobina + [ApiController] + [Authorize] + public class TiposBobinaController : ControllerBase + { + private readonly ITipoBobinaService _tipoBobinaService; + private readonly ILogger _logger; + + public TiposBobinaController(ITipoBobinaService tipoBobinaService, ILogger logger) + { + _tipoBobinaService = tipoBobinaService ?? throw new ArgumentNullException(nameof(tipoBobinaService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + // --- Helper para permisos --- + private bool TienePermiso(string codAccRequerido) + { + if (User.IsInRole("SuperAdmin")) return true; + return User.HasClaim(c => c.Type == "permission" && c.Value == codAccRequerido); + } + + // --- Helper para User ID --- + private int? GetCurrentUserId() + { + var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub"); + if (int.TryParse(userIdClaim, out int userId)) return userId; + _logger.LogWarning("No se pudo obtener el UserId del token JWT en TiposBobinaController."); + return null; + } + + // GET: api/tiposbobina + // Permiso: IB006 (Ver Tipos Bobinas) + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetAllTiposBobina([FromQuery] string? denominacion) + { + if (!TienePermiso("IB006")) return Forbid(); + + try + { + var tiposBobina = await _tipoBobinaService.ObtenerTodosAsync(denominacion); + return Ok(tiposBobina); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Tipos de Bobina. Filtro: {Denominacion}", denominacion); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener los tipos de bobina."); + } + } + + // GET: api/tiposbobina/{id} + // Permiso: IB006 (Ver Tipos Bobinas) + [HttpGet("{id:int}", Name = "GetTipoBobinaById")] + [ProducesResponseType(typeof(TipoBobinaDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetTipoBobinaById(int id) + { + if (!TienePermiso("IB006")) return Forbid(); + + try + { + var tipoBobina = await _tipoBobinaService.ObtenerPorIdAsync(id); + if (tipoBobina == null) return NotFound(new { message = $"Tipo de bobina con ID {id} no encontrado." }); + return Ok(tipoBobina); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Tipo de Bobina por ID: {Id}", id); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al obtener el tipo de bobina."); + } + } + + // POST: api/tiposbobina + // Permiso: IB007 (Agregar Tipos Bobinas) + [HttpPost] + [ProducesResponseType(typeof(TipoBobinaDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreateTipoBobina([FromBody] CreateTipoBobinaDto createDto) + { + if (!TienePermiso("IB007")) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (tipoBobinaCreado, error) = await _tipoBobinaService.CrearAsync(createDto, idUsuario.Value); + if (error != null) return BadRequest(new { message = error }); + if (tipoBobinaCreado == null) return StatusCode(StatusCodes.Status500InternalServerError, "Error al crear el tipo de bobina."); + return CreatedAtRoute("GetTipoBobinaById", new { id = tipoBobinaCreado.IdTipoBobina }, tipoBobinaCreado); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear Tipo de Bobina. Denominación: {Denominacion} por Usuario ID: {UsuarioId}", createDto.Denominacion, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al crear el tipo de bobina."); + } + } + + // PUT: api/tiposbobina/{id} + // Permiso: IB008 (Modificar Tipos Bobinas) + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task UpdateTipoBobina(int id, [FromBody] UpdateTipoBobinaDto updateDto) + { + if (!TienePermiso("IB008")) return Forbid(); + if (!ModelState.IsValid) return BadRequest(ModelState); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (exito, error) = await _tipoBobinaService.ActualizarAsync(id, updateDto, idUsuario.Value); + if (!exito) + { + if (error == "Tipo de bobina no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al actualizar Tipo de Bobina ID: {Id} por Usuario ID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al actualizar el tipo de bobina."); + } + } + + // DELETE: api/tiposbobina/{id} + // Permiso: IB009 (Eliminar Tipos Bobinas) + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] // Si está en uso + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task DeleteTipoBobina(int id) + { + if (!TienePermiso("IB009")) return Forbid(); + var idUsuario = GetCurrentUserId(); + if (idUsuario == null) return Unauthorized("Token inválido."); + + try + { + var (exito, error) = await _tipoBobinaService.EliminarAsync(id, idUsuario.Value); + if (!exito) + { + if (error == "Tipo de bobina no encontrado.") return NotFound(new { message = error }); + return BadRequest(new { message = error }); // Ej: "En uso" + } + return NoContent(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al eliminar Tipo de Bobina ID: {Id} por Usuario ID: {UsuarioId}", id, idUsuario); + return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al eliminar el tipo de bobina."); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Contables/ISaldoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/ISaldoRepository.cs new file mode 100644 index 0000000..806a6b7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/ISaldoRepository.cs @@ -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> GetAllDistribuidorIdsAsync(); + // Método para crear el saldo inicial (podría devolver bool o int) + Task 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 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 ModificarSaldoAsync(string destino, int idDestino, int idEmpresa, decimal montoAAgregar, IDbTransaction? transaction = null); + Task CheckIfSaldosExistForEmpresaAsync(int id); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/ITipoPagoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/ITipoPagoRepository.cs similarity index 92% rename from Backend/GestionIntegral.Api/Data/Repositories/ITipoPagoRepository.cs rename to Backend/GestionIntegral.Api/Data/Repositories/Contables/ITipoPagoRepository.cs index e8f7c7b..8c9ca08 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/ITipoPagoRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/ITipoPagoRepository.cs @@ -2,7 +2,7 @@ using GestionIntegral.Api.Models.Contables; // Para TipoPago y TipoPagoHistorico using System.Collections.Generic; using System.Threading.Tasks; -namespace GestionIntegral.Api.Data.Repositories +namespace GestionIntegral.Api.Data.Repositories.Contables { public interface ITipoPagoRepository { diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Contables/SaldoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/SaldoRepository.cs new file mode 100644 index 0000000..308ff6f --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/SaldoRepository.cs @@ -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 _logger; + + public SaldoRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllDistribuidorIdsAsync() + { + var sql = "SELECT Id_Distribuidor FROM dbo.dist_dtDistribuidores"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + return await connection.QueryAsync(sql); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener IDs de Distribuidores."); + return Enumerable.Empty(); + } + } + + public async Task 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 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 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 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(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. + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/TipoPagoRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Contables/TipoPagoRepository.cs similarity index 99% rename from Backend/GestionIntegral.Api/Data/Repositories/TipoPagoRepository.cs rename to Backend/GestionIntegral.Api/Data/Repositories/Contables/TipoPagoRepository.cs index 6fc8d77..164d7bf 100644 --- a/Backend/GestionIntegral.Api/Data/Repositories/TipoPagoRepository.cs +++ b/Backend/GestionIntegral.Api/Data/Repositories/Contables/TipoPagoRepository.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Data; using System.Threading.Tasks; -namespace GestionIntegral.Api.Data.Repositories // O GestionIntegral.Api.Repositories +namespace GestionIntegral.Api.Data.Repositories.Contables { public class TipoPagoRepository : ITipoPagoRepository { diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EmpresaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EmpresaRepository.cs new file mode 100644 index 0000000..616d655 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/EmpresaRepository.cs @@ -0,0 +1,220 @@ +using Dapper; +using GestionIntegral.Api.Data.Repositories; +using GestionIntegral.Api.Models.Distribucion; +using System.Collections.Generic; +using System.Data; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class EmpresaRepository : IEmpresaRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public EmpresaRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + // --- Métodos de Lectura (no necesitan transacción explícita aquí) --- + + public async Task> GetAllAsync(string? nombreFilter, string? detalleFilter) + { + var sqlBuilder = new StringBuilder("SELECT Id_Empresa AS IdEmpresa, Nombre, Detalle FROM dbo.dist_dtEmpresas WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(nombreFilter)) + { + sqlBuilder.Append(" AND Nombre LIKE @NombreFilter"); + parameters.Add("NombreFilter", $"%{nombreFilter}%"); + } + if (!string.IsNullOrWhiteSpace(detalleFilter)) + { + sqlBuilder.Append(" AND Detalle LIKE @DetalleFilter"); + parameters.Add("DetalleFilter", $"%{detalleFilter}%"); + } + sqlBuilder.Append(" ORDER BY Nombre;"); + + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todas las Empresas."); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int id) + { + var sql = "SELECT Id_Empresa AS IdEmpresa, Nombre, Detalle FROM dbo.dist_dtEmpresas WHERE Id_Empresa = @Id"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Empresa por ID: {IdEmpresa}", id); + return null; + } + } + + public async Task ExistsByNameAsync(string nombre, int? excludeId = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtEmpresas WHERE Nombre = @Nombre"); + var parameters = new DynamicParameters(); + parameters.Add("Nombre", nombre); + + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND Id_Empresa != @ExcludeId"); + parameters.Add("ExcludeId", excludeId.Value); + } + + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + var count = await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + return count > 0; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByNameAsync para Empresa con nombre: {Nombre}", nombre); + return true; // Asumir que existe si hay error + } + } + + public async Task IsInUseAsync(int id) + { + // Verifica si la empresa está referenciada en dist_dtPublicaciones + var sql = "SELECT COUNT(1) FROM dbo.dist_dtPublicaciones WHERE Id_Empresa = @IdEmpresa"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + var count = await connection.ExecuteScalarAsync(sql, new { IdEmpresa = id }); + return count > 0; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en IsInUseAsync para Empresa ID: {IdEmpresa}", id); + return true; // Asumir en uso si hay error + } + } + + // --- Métodos de Escritura (USAN TRANSACCIÓN PASADA DESDE EL SERVICIO) --- + + public async Task CreateAsync(Empresa nuevaEmpresa, int idUsuario, IDbTransaction transaction) + { + var sqlInsert = @" + INSERT INTO dbo.dist_dtEmpresas (Nombre, Detalle) + OUTPUT INSERTED.Id_Empresa AS IdEmpresa, INSERTED.Nombre, INSERTED.Detalle + VALUES (@Nombre, @Detalle);"; + + var sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtEmpresas_H (Id_Empresa, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdEmpresa, @Nombre, @Detalle, @IdUsuario, @FechaMod, @TipoMod);"; + + // La ejecución y manejo de errores/commit/rollback se hará en el SERVICIO + // Aquí solo ejecutamos los comandos DENTRO de la transacción existente. + + // Obtener el ID y datos insertados + var insertedEmpresa = await transaction.Connection!.QuerySingleAsync( + sqlInsert, + new { nuevaEmpresa.Nombre, nuevaEmpresa.Detalle }, + transaction: transaction + ); + + if (insertedEmpresa == null || insertedEmpresa.IdEmpresa <= 0) + { + throw new Exception("No se pudo obtener el ID de la empresa insertada."); + } + + // Insertar en historial + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new { + IdEmpresa = insertedEmpresa.IdEmpresa, + insertedEmpresa.Nombre, + insertedEmpresa.Detalle, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Insertada" + }, transaction: transaction); + + return insertedEmpresa; // Devolver la entidad con el ID + } + + public async Task UpdateAsync(Empresa empresaAActualizar, int idUsuario, IDbTransaction transaction) + { + // El servicio ya verificó que existe. Obtenemos estado actual para historial. + var empresaActual = await GetByIdAsync(empresaAActualizar.IdEmpresa); // Podría fallar si ya no existe, controlar en servicio + if (empresaActual == null) throw new InvalidOperationException("No se encontró la empresa para obtener datos para el historial de actualización."); + + var sqlUpdate = @" + UPDATE dbo.dist_dtEmpresas + SET Nombre = @Nombre, Detalle = @Detalle + WHERE Id_Empresa = @IdEmpresa;"; + + var sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtEmpresas_H (Id_Empresa, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdEmpresa, @NombreActual, @DetalleActual, @IdUsuario, @FechaMod, @TipoMod);"; + + // Insertar en historial (estado anterior) + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new { + IdEmpresa = empresaActual.IdEmpresa, + NombreActual = empresaActual.Nombre, + DetalleActual = empresaActual.Detalle, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Modificada" + }, transaction: transaction); + + // Actualizar principal + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlUpdate, new { + empresaAActualizar.Nombre, + empresaAActualizar.Detalle, + empresaAActualizar.IdEmpresa + }, transaction: transaction); + + return rowsAffected == 1; + } + + public async Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction) + { + var empresaActual = await GetByIdAsync(id); // Obtener datos para historial + if (empresaActual == null) throw new InvalidOperationException("No se encontró la empresa para obtener datos para el historial de eliminación."); + + var sqlDelete = "DELETE FROM dbo.dist_dtEmpresas WHERE Id_Empresa = @Id"; + var sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtEmpresas_H (Id_Empresa, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdEmpresa, @Nombre, @Detalle, @IdUsuario, @FechaMod, @TipoMod);"; + + // Insertar en historial (estado antes de borrar) + await transaction.Connection!.ExecuteAsync(sqlInsertHistorico, new { + IdEmpresa = empresaActual.IdEmpresa, + empresaActual.Nombre, + empresaActual.Detalle, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Eliminada" + }, transaction: transaction); + + // Eliminar de la tabla principal + var rowsAffected = await transaction.Connection!.ExecuteAsync(sqlDelete, new { Id = id }, transaction: transaction); + + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEmpresaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEmpresaRepository.cs new file mode 100644 index 0000000..a06b2a6 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IEmpresaRepository.cs @@ -0,0 +1,18 @@ +using GestionIntegral.Api.Models.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; // Para IDbTransaction + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IEmpresaRepository + { + Task> GetAllAsync(string? nombreFilter, string? detalleFilter); + Task GetByIdAsync(int id); + Task CreateAsync(Empresa nuevaEmpresa, int idUsuario, IDbTransaction transaction); // Necesita transacción + Task UpdateAsync(Empresa empresaAActualizar, int idUsuario, IDbTransaction transaction); // Necesita transacción + Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction); // Necesita transacción + Task ExistsByNameAsync(string nombre, int? excludeId = null); + Task IsInUseAsync(int id); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IZonaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IZonaRepository.cs new file mode 100644 index 0000000..3b46136 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/IZonaRepository.cs @@ -0,0 +1,17 @@ +using GestionIntegral.Api.Models.Distribucion; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public interface IZonaRepository + { + Task> GetAllAsync(string? nombreFilter, string? descripcionFilter, bool soloActivas = true); + Task GetByIdAsync(int id, bool soloActivas = true); + Task CreateAsync(Zona nuevaZona, int idUsuario); + Task UpdateAsync(Zona zonaAActualizar, int idUsuario); + Task SoftDeleteAsync(int id, int idUsuario); // Cambiado de DeleteAsync a SoftDeleteAsync + Task ExistsByNameAsync(string nombre, int? excludeId = null, bool soloActivas = true); + Task IsInUseAsync(int id); + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ZonaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ZonaRepository.cs new file mode 100644 index 0000000..96079d7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Distribucion/ZonaRepository.cs @@ -0,0 +1,305 @@ +using Dapper; +using GestionIntegral.Api.Models.Distribucion; +using System.Collections.Generic; +using System.Data; +using System.Text; // Para StringBuilder +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Distribucion +{ + public class ZonaRepository : IZonaRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public ZonaRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? nombreFilter, string? descripcionFilter, bool soloActivas = true) + { + var sqlBuilder = new StringBuilder("SELECT Id_Zona AS IdZona, Nombre, Descripcion, Estado FROM dbo.dist_dtZonas WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (soloActivas) + { + sqlBuilder.Append(" AND Estado = 1"); + } + if (!string.IsNullOrWhiteSpace(nombreFilter)) + { + sqlBuilder.Append(" AND Nombre LIKE @NombreFilter"); + parameters.Add("NombreFilter", $"%{nombreFilter}%"); + } + if (!string.IsNullOrWhiteSpace(descripcionFilter)) + { + sqlBuilder.Append(" AND Descripcion LIKE @DescripcionFilter"); + parameters.Add("DescripcionFilter", $"%{descripcionFilter}%"); + } + sqlBuilder.Append(" ORDER BY Nombre;"); + + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todas las Zonas. Filtros: Nombre={NombreFilter}, Descripcion={DescFilter}", nombreFilter, descripcionFilter); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int id, bool soloActivas = true) + { + var sqlBuilder = new StringBuilder("SELECT Id_Zona AS IdZona, Nombre, Descripcion, Estado FROM dbo.dist_dtZonas WHERE Id_Zona = @Id"); + if (soloActivas) + { + sqlBuilder.Append(" AND Estado = 1"); + } + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + return await connection.QuerySingleOrDefaultAsync(sqlBuilder.ToString(), new { Id = id }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Zona por ID: {IdZona}", id); + return null; + } + } + + public async Task ExistsByNameAsync(string nombre, int? excludeId = null, bool soloActivas = true) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_dtZonas WHERE Nombre = @Nombre"); + var parameters = new DynamicParameters(); + parameters.Add("Nombre", nombre); + + if (soloActivas) + { + sqlBuilder.Append(" AND Estado = 1"); + } + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND Id_Zona != @ExcludeId"); + parameters.Add("ExcludeId", excludeId.Value); + } + + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + var count = await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + return count > 0; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByNameAsync para Zona con nombre: {Nombre}", nombre); + return true; + } + } + + public async Task CreateAsync(Zona nuevaZona, int idUsuario) + { + // Las nuevas zonas siempre se crean con Estado = 1 + var sqlInsert = @" + INSERT INTO dbo.dist_dtZonas (Nombre, Descripcion, Estado) + OUTPUT INSERTED.Id_Zona AS IdZona, INSERTED.Nombre, INSERTED.Descripcion, INSERTED.Estado + VALUES (@Nombre, @Descripcion, 1);"; + + var sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtZonas_H (Id_Zona, Nombre, Descripcion, Estado, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdZona, @Nombre, @Descripcion, @Estado, @IdUsuario, @FechaMod, @TipoMod);"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + { + try + { + var insertedZona = await connection.QuerySingleAsync( + sqlInsert, + new { nuevaZona.Nombre, nuevaZona.Descripcion }, + transaction: transaction + ); + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdZona = insertedZona.IdZona, + insertedZona.Nombre, + insertedZona.Descripcion, + Estado = insertedZona.Estado, // Será true (1) + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Insertada" + }, transaction: transaction); + + transaction.Commit(); + return insertedZona; + } + catch (Exception exTrans) + { + transaction.Rollback(); + _logger.LogError(exTrans, "Error en transacción CreateAsync para Zona. Nombre: {Nombre}", nuevaZona.Nombre); + return null; + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error general en CreateAsync para Zona. Nombre: {Nombre}", nuevaZona.Nombre); + return null; + } + } + + public async Task UpdateAsync(Zona zonaAActualizar, int idUsuario) + { + var zonaActual = await GetByIdAsync(zonaAActualizar.IdZona, soloActivas: false); // Obtener incluso si está inactiva para el historial + if (zonaActual == null) return false; + + // Solo actualizamos si está activa, pero el historial registra el estado que tenía. + // El servicio decidirá si se puede actualizar una zona inactiva. + var sqlUpdate = @" + UPDATE dbo.dist_dtZonas + SET Nombre = @Nombre, Descripcion = @Descripcion + WHERE Id_Zona = @IdZona;"; // Podríamos añadir AND Estado = 1 si el servicio lo requiere así estrictamente. + + var sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtZonas_H (Id_Zona, Nombre, Descripcion, Estado, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdZona, @NombreActual, @DescripcionActual, @EstadoActual, @IdUsuario, @FechaMod, @TipoMod);"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + { + try + { + // Registrar el estado *antes* de la modificación + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdZona = zonaActual.IdZona, + NombreActual = zonaActual.Nombre, + DescripcionActual = zonaActual.Descripcion, + EstadoActual = zonaActual.Estado, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Modificada" + }, transaction: transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new + { + zonaAActualizar.Nombre, + zonaAActualizar.Descripcion, + zonaAActualizar.IdZona + }, transaction: transaction); + + transaction.Commit(); + return rowsAffected == 1; + } + catch (Exception exTrans) + { + transaction.Rollback(); + _logger.LogError(exTrans, "Error en transacción UpdateAsync para Zona ID: {IdZona}", zonaAActualizar.IdZona); + return false; + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error general en UpdateAsync para Zona ID: {IdZona}", zonaAActualizar.IdZona); + return false; + } + } + + public async Task SoftDeleteAsync(int id, int idUsuario) + { + var zonaActual = await GetByIdAsync(id, soloActivas: false); // Obtenerla sin importar su estado para el historial + if (zonaActual == null) return false; + if (!zonaActual.Estado) return true; // Ya está "eliminada" (inactiva), considerar éxito + + var sqlSoftDelete = @"UPDATE dbo.dist_dtZonas SET Estado = 0 WHERE Id_Zona = @IdZona;"; + var sqlInsertHistorico = @" + INSERT INTO dbo.dist_dtZonas_H (Id_Zona, Nombre, Descripcion, Estado, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdZona, @Nombre, @Descripcion, @EstadoNuevo, @IdUsuario, @FechaMod, @TipoMod);"; + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + { + try + { + // Actualizar la tabla principal + var rowsAffected = await connection.ExecuteAsync(sqlSoftDelete, new { IdZona = id }, transaction: transaction); + + if (rowsAffected == 1) + { + // Registrar en el historial con el nuevo estado (0) + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdZona = zonaActual.IdZona, + zonaActual.Nombre, + zonaActual.Descripcion, + EstadoNuevo = false, // El estado después del soft delete + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Eliminada" // O "Deshabilitada" + }, transaction: transaction); + } + + transaction.Commit(); + return rowsAffected == 1; + } + catch (Exception exTrans) + { + transaction.Rollback(); + _logger.LogError(exTrans, "Error en transacción SoftDeleteAsync para Zona ID: {IdZona}", id); + return false; + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error general en SoftDeleteAsync para Zona ID: {IdZona}", id); + return false; + } + } + + public async Task IsInUseAsync(int id) + { + // Verifica en dist_dtDistribuidores y dist_dtCanillas + var sqlCheckDist = "SELECT COUNT(1) FROM dbo.dist_dtDistribuidores WHERE Id_Zona = @IdZona"; + var sqlCheckCanillas = "SELECT COUNT(1) FROM dbo.dist_dtCanillas WHERE Id_Zona = @IdZona AND Baja = 0"; // Solo canillas activos + try + { + using (var connection = _connectionFactory.CreateConnection()) + { + var countDist = await connection.ExecuteScalarAsync(sqlCheckDist, new { IdZona = id }); + if (countDist > 0) return true; + + var countCanillas = await connection.ExecuteScalarAsync(sqlCheckCanillas, new { IdZona = id }); + return countCanillas > 0; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en IsInUseAsync para Zona ID: {IdZona}", id); + return true; // Asumir que está en uso si hay error + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/EstadoBobinaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/EstadoBobinaRepository.cs new file mode 100644 index 0000000..3275eff --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/EstadoBobinaRepository.cs @@ -0,0 +1,208 @@ +using Dapper; +using GestionIntegral.Api.Data.Repositories.Impresion; +using GestionIntegral.Api.Models.Impresion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Impresion +{ + public class EstadoBobinaRepository : IEstadoBobinaRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public EstadoBobinaRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? denominacionFilter) + { + var sqlBuilder = new StringBuilder("SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion, Obs FROM dbo.bob_dtEstadosBobinas WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(denominacionFilter)) + { + sqlBuilder.Append(" AND Denominacion LIKE @DenominacionFilter"); + parameters.Add("DenominacionFilter", $"%{denominacionFilter}%"); + } + sqlBuilder.Append(" ORDER BY Denominacion;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Estados de Bobina. Filtro: {Denominacion}", denominacionFilter); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int id) + { + const string sql = "SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion, Obs FROM dbo.bob_dtEstadosBobinas WHERE Id_EstadoBobina = @Id"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Estado de Bobina por ID: {IdEstadoBobina}", id); + return null; + } + } + + public async Task ExistsByDenominacionAsync(string denominacion, int? excludeId = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.bob_dtEstadosBobinas WHERE Denominacion = @Denominacion"); + var parameters = new DynamicParameters(); + parameters.Add("Denominacion", denominacion); + + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND Id_EstadoBobina != @ExcludeId"); + parameters.Add("ExcludeId", excludeId.Value); + } + + try + { + using var connection = _connectionFactory.CreateConnection(); + var count = await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + return count > 0; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByDenominacionAsync para Estado de Bobina con denominación: {Denominacion}", denominacion); + return true; // Asumir que existe en caso de error + } + } + + public async Task IsInUseAsync(int id) + { + // Verificar si el estado de bobina está referenciado en bob_StockBobinas + const string sqlCheckStock = "SELECT TOP 1 1 FROM dbo.bob_StockBobinas WHERE Id_EstadoBobina = @IdEstadoBobina"; + try + { + using var connection = _connectionFactory.CreateConnection(); + var inStock = await connection.ExecuteScalarAsync(sqlCheckStock, new { IdEstadoBobina = id }); + return inStock.HasValue && inStock.Value == 1; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en IsInUseAsync para Estado de Bobina ID: {IdEstadoBobina}", id); + return true; // Asumir en uso si hay error + } + } + + // --- Métodos de Escritura (USAN TRANSACCIÓN) --- + + public async Task CreateAsync(EstadoBobina nuevoEstadoBobina, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.bob_dtEstadosBobinas (Denominacion, Obs) + OUTPUT INSERTED.Id_EstadoBobina AS IdEstadoBobina, INSERTED.Denominacion, INSERTED.Obs + VALUES (@Denominacion, @Obs);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_dtEstadosBobinas_H (Id_EstadoBobina, Denominacion, Obs, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdEstadoBobina, @Denominacion, @Obs, @IdUsuario, @FechaMod, @TipoMod);"; + + var connection = transaction.Connection!; + + var insertedEstado = await connection.QuerySingleAsync( + sqlInsert, + new { nuevoEstadoBobina.Denominacion, nuevoEstadoBobina.Obs }, + transaction: transaction + ); + + if (insertedEstado == null || insertedEstado.IdEstadoBobina <= 0) + { + throw new DataException("No se pudo obtener el ID del estado de bobina insertado."); + } + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdEstadoBobina = insertedEstado.IdEstadoBobina, + insertedEstado.Denominacion, + insertedEstado.Obs, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Insertada" + }, transaction: transaction); + + return insertedEstado; + } + + public async Task UpdateAsync(EstadoBobina estadoBobinaAActualizar, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var estadoActual = await connection.QuerySingleOrDefaultAsync( + "SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion, Obs FROM dbo.bob_dtEstadosBobinas WHERE Id_EstadoBobina = @Id", + new { Id = estadoBobinaAActualizar.IdEstadoBobina }, + transaction); + + if (estadoActual == null) throw new KeyNotFoundException($"No se encontró el estado de bobina con ID {estadoBobinaAActualizar.IdEstadoBobina} para actualizar."); + + const string sqlUpdate = "UPDATE dbo.bob_dtEstadosBobinas SET Denominacion = @Denominacion, Obs = @Obs WHERE Id_EstadoBobina = @IdEstadoBobina;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_dtEstadosBobinas_H (Id_EstadoBobina, Denominacion, Obs, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdEstadoBobina, @DenominacionActual, @ObsActual, @IdUsuario, @FechaMod, @TipoMod);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdEstadoBobina = estadoActual.IdEstadoBobina, + DenominacionActual = estadoActual.Denominacion, // Valor ANTES + ObsActual = estadoActual.Obs, // Valor ANTES + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Modificada" + }, transaction: transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new + { + estadoBobinaAActualizar.Denominacion, + estadoBobinaAActualizar.Obs, + estadoBobinaAActualizar.IdEstadoBobina + }, transaction: transaction); + + return rowsAffected == 1; + } + + public async Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var estadoActual = await connection.QuerySingleOrDefaultAsync( + "SELECT Id_EstadoBobina AS IdEstadoBobina, Denominacion, Obs FROM dbo.bob_dtEstadosBobinas WHERE Id_EstadoBobina = @Id", + new { Id = id }, + transaction); + + if (estadoActual == null) throw new KeyNotFoundException($"No se encontró el estado de bobina con ID {id} para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.bob_dtEstadosBobinas WHERE Id_EstadoBobina = @Id"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_dtEstadosBobinas_H (Id_EstadoBobina, Denominacion, Obs, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdEstadoBobina, @Denominacion, @Obs, @IdUsuario, @FechaMod, @TipoMod);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdEstadoBobina = estadoActual.IdEstadoBobina, + estadoActual.Denominacion, // Valor ANTES + estadoActual.Obs, // Valor ANTES + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Eliminada" + }, transaction: transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction: transaction); + + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IEstadoBobinaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IEstadoBobinaRepository.cs new file mode 100644 index 0000000..69fcca7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IEstadoBobinaRepository.cs @@ -0,0 +1,18 @@ +using GestionIntegral.Api.Models.Impresion; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; + +namespace GestionIntegral.Api.Data.Repositories.Impresion +{ + public interface IEstadoBobinaRepository + { + Task> GetAllAsync(string? denominacionFilter); + Task GetByIdAsync(int id); + Task CreateAsync(EstadoBobina nuevoEstadoBobina, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(EstadoBobina estadoBobinaAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction); + Task ExistsByDenominacionAsync(string denominacion, int? excludeId = null); + Task IsInUseAsync(int id); // Verificar si se usa en bob_StockBobinas + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IPlantaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IPlantaRepository.cs new file mode 100644 index 0000000..505a4e7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/IPlantaRepository.cs @@ -0,0 +1,18 @@ +using GestionIntegral.Api.Models.Impresion; // Para Planta +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; // Para IDbTransaction + +namespace GestionIntegral.Api.Data.Repositories.Impresion +{ + public interface IPlantaRepository + { + Task> GetAllAsync(string? nombreFilter, string? detalleFilter); + Task GetByIdAsync(int id); + Task CreateAsync(Planta nuevaPlanta, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(Planta plantaAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction); // Borrado físico con historial + Task ExistsByNameAsync(string nombre, int? excludeId = null); + Task IsInUseAsync(int id); // Verificar si se usa en bob_StockBobinas o bob_RegPublicaciones + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/ITipoBobinaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/ITipoBobinaRepository.cs new file mode 100644 index 0000000..fa3f4c0 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/ITipoBobinaRepository.cs @@ -0,0 +1,18 @@ +using GestionIntegral.Api.Models.Impresion; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Data; // Para IDbTransaction + +namespace GestionIntegral.Api.Data.Repositories.Impresion +{ + public interface ITipoBobinaRepository + { + Task> GetAllAsync(string? denominacionFilter); + Task GetByIdAsync(int id); + Task CreateAsync(TipoBobina nuevoTipoBobina, int idUsuario, IDbTransaction transaction); + Task UpdateAsync(TipoBobina tipoBobinaAActualizar, int idUsuario, IDbTransaction transaction); + Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction); + Task ExistsByDenominacionAsync(string denominacion, int? excludeId = null); + Task IsInUseAsync(int id); // Verificar si se usa en bob_StockBobinas + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/PlantaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/PlantaRepository.cs new file mode 100644 index 0000000..b21ea66 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/PlantaRepository.cs @@ -0,0 +1,229 @@ +using Dapper; +using GestionIntegral.Api.Models.Impresion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Text; // Para StringBuilder +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Impresion +{ + public class PlantaRepository : IPlantaRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public PlantaRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? nombreFilter, string? detalleFilter) + { + var sqlBuilder = new StringBuilder("SELECT Id_Planta AS IdPlanta, Nombre, Detalle FROM dbo.bob_dtPlantas WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(nombreFilter)) + { + sqlBuilder.Append(" AND Nombre LIKE @NombreFilter"); + parameters.Add("NombreFilter", $"%{nombreFilter}%"); + } + if (!string.IsNullOrWhiteSpace(detalleFilter)) + { + sqlBuilder.Append(" AND Detalle LIKE @DetalleFilter"); + parameters.Add("DetalleFilter", $"%{detalleFilter}%"); + } + sqlBuilder.Append(" ORDER BY Nombre;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todas las Plantas. Filtros: Nombre={Nombre}, Detalle={Detalle}", nombreFilter, detalleFilter); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int id) + { + const string sql = "SELECT Id_Planta AS IdPlanta, Nombre, Detalle FROM dbo.bob_dtPlantas WHERE Id_Planta = @Id"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Planta por ID: {IdPlanta}", id); + return null; + } + } + + public async Task ExistsByNameAsync(string nombre, int? excludeId = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.bob_dtPlantas WHERE Nombre = @Nombre"); + var parameters = new DynamicParameters(); + parameters.Add("Nombre", nombre); + + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND Id_Planta != @ExcludeId"); + parameters.Add("ExcludeId", excludeId.Value); + } + + try + { + using var connection = _connectionFactory.CreateConnection(); + var count = await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + return count > 0; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByNameAsync para Planta con nombre: {Nombre}", nombre); + // Asumir que existe en caso de error para prevenir duplicados accidentales + return true; + } + } + + public async Task IsInUseAsync(int id) + { + // Verificar si la planta está referenciada en bob_StockBobinas O bob_RegPublicaciones + const string sqlCheckStock = "SELECT TOP 1 1 FROM dbo.bob_StockBobinas WHERE Id_Planta = @IdPlanta"; + const string sqlCheckRegPubli = "SELECT TOP 1 1 FROM dbo.bob_RegPublicaciones WHERE Id_Planta = @IdPlanta"; + + try + { + using var connection = _connectionFactory.CreateConnection(); + var inStock = await connection.ExecuteScalarAsync(sqlCheckStock, new { IdPlanta = id }); + if (inStock.HasValue && inStock.Value == 1) return true; + + var inRegPubli = await connection.ExecuteScalarAsync(sqlCheckRegPubli, new { IdPlanta = id }); + return inRegPubli.HasValue && inRegPubli.Value == 1; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en IsInUseAsync para Planta ID: {IdPlanta}", id); + // Asumir que está en uso si hay error para prevenir borrado incorrecto + return true; + } + } + + // --- Métodos de Escritura (USAN TRANSACCIÓN PASADA DESDE EL SERVICIO) --- + + public async Task CreateAsync(Planta nuevaPlanta, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.bob_dtPlantas (Nombre, Detalle) + OUTPUT INSERTED.Id_Planta AS IdPlanta, INSERTED.Nombre, INSERTED.Detalle + VALUES (@Nombre, @Detalle);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_dtPlantas_H (Id_Planta, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPlanta, @Nombre, @Detalle, @IdUsuario, @FechaMod, @TipoMod);"; + + // Dapper requiere que la conexión esté asociada a la transacción + var connection = transaction.Connection ?? throw new InvalidOperationException("Transaction has no associated connection."); + + var insertedPlanta = await connection.QuerySingleAsync( + sqlInsert, + new { nuevaPlanta.Nombre, nuevaPlanta.Detalle }, + transaction: transaction // Pasar la transacción + ); + + if (insertedPlanta == null || insertedPlanta.IdPlanta <= 0) + { + throw new DataException("No se pudo obtener el ID de la planta insertada."); // Usar DataException + } + + // Insertar en historial + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdPlanta = insertedPlanta.IdPlanta, + insertedPlanta.Nombre, + insertedPlanta.Detalle, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Insertada" + }, transaction: transaction); + + return insertedPlanta; // Devolver la entidad con el ID + } + + public async Task UpdateAsync(Planta plantaAActualizar, int idUsuario, IDbTransaction transaction) + { + // El servicio ya verificó que existe. Obtener estado actual para historial dentro de la transacción. + var connection = transaction.Connection!; + var plantaActual = await connection.QuerySingleOrDefaultAsync( + "SELECT Id_Planta AS IdPlanta, Nombre, Detalle FROM dbo.bob_dtPlantas WHERE Id_Planta = @Id", + new { Id = plantaAActualizar.IdPlanta }, + transaction); + + if (plantaActual == null) throw new KeyNotFoundException($"No se encontró la planta con ID {plantaAActualizar.IdPlanta} para actualizar."); // Más específico + + const string sqlUpdate = @" + UPDATE dbo.bob_dtPlantas + SET Nombre = @Nombre, Detalle = @Detalle + WHERE Id_Planta = @IdPlanta;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_dtPlantas_H (Id_Planta, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPlanta, @NombreActual, @DetalleActual, @IdUsuario, @FechaMod, @TipoMod);"; + + // Insertar en historial (estado anterior) + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdPlanta = plantaActual.IdPlanta, + NombreActual = plantaActual.Nombre, + DetalleActual = plantaActual.Detalle, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Modificada" + }, transaction: transaction); + + // Actualizar principal + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new + { + plantaAActualizar.Nombre, + plantaAActualizar.Detalle, + plantaAActualizar.IdPlanta + }, transaction: transaction); + + return rowsAffected == 1; + } + + public async Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + // Obtener datos para historial ANTES de borrar + var plantaActual = await connection.QuerySingleOrDefaultAsync( + "SELECT Id_Planta AS IdPlanta, Nombre, Detalle FROM dbo.bob_dtPlantas WHERE Id_Planta = @Id", + new { Id = id }, + transaction); + + if (plantaActual == null) throw new KeyNotFoundException($"No se encontró la planta con ID {id} para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.bob_dtPlantas WHERE Id_Planta = @Id"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_dtPlantas_H (Id_Planta, Nombre, Detalle, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdPlanta, @Nombre, @Detalle, @IdUsuario, @FechaMod, @TipoMod);"; + + // Insertar en historial (estado antes de borrar) + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdPlanta = plantaActual.IdPlanta, + plantaActual.Nombre, + plantaActual.Detalle, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Eliminada" + }, transaction: transaction); + + // Eliminar de la tabla principal + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction: transaction); + + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Data/Repositories/Impresion/TipoBobinaRepository.cs b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/TipoBobinaRepository.cs new file mode 100644 index 0000000..05948f9 --- /dev/null +++ b/Backend/GestionIntegral.Api/Data/Repositories/Impresion/TipoBobinaRepository.cs @@ -0,0 +1,203 @@ +using Dapper; +using GestionIntegral.Api.Models.Impresion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Data.Repositories.Impresion +{ + public class TipoBobinaRepository : ITipoBobinaRepository + { + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public TipoBobinaRepository(DbConnectionFactory connectionFactory, ILogger logger) + { + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> GetAllAsync(string? denominacionFilter) + { + var sqlBuilder = new StringBuilder("SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE 1=1"); + var parameters = new DynamicParameters(); + + if (!string.IsNullOrWhiteSpace(denominacionFilter)) + { + sqlBuilder.Append(" AND Denominacion LIKE @DenominacionFilter"); + parameters.Add("DenominacionFilter", $"%{denominacionFilter}%"); + } + sqlBuilder.Append(" ORDER BY Denominacion;"); + + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QueryAsync(sqlBuilder.ToString(), parameters); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener todos los Tipos de Bobina. Filtro: {Denominacion}", denominacionFilter); + return Enumerable.Empty(); + } + } + + public async Task GetByIdAsync(int id) + { + const string sql = "SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE Id_TipoBobina = @Id"; + try + { + using var connection = _connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync(sql, new { Id = id }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener Tipo de Bobina por ID: {IdTipoBobina}", id); + return null; + } + } + + public async Task ExistsByDenominacionAsync(string denominacion, int? excludeId = null) + { + var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.bob_dtBobinas WHERE Denominacion = @Denominacion"); + var parameters = new DynamicParameters(); + parameters.Add("Denominacion", denominacion); + + if (excludeId.HasValue) + { + sqlBuilder.Append(" AND Id_TipoBobina != @ExcludeId"); + parameters.Add("ExcludeId", excludeId.Value); + } + + try + { + using var connection = _connectionFactory.CreateConnection(); + var count = await connection.ExecuteScalarAsync(sqlBuilder.ToString(), parameters); + return count > 0; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en ExistsByDenominacionAsync para Tipo de Bobina con denominación: {Denominacion}", denominacion); + return true; // Asumir que existe en caso de error + } + } + + public async Task IsInUseAsync(int id) + { + // Verificar si el tipo de bobina está referenciado en bob_StockBobinas + const string sqlCheckStock = "SELECT TOP 1 1 FROM dbo.bob_StockBobinas WHERE Id_TipoBobina = @IdTipoBobina"; + try + { + using var connection = _connectionFactory.CreateConnection(); + var inStock = await connection.ExecuteScalarAsync(sqlCheckStock, new { IdTipoBobina = id }); + return inStock.HasValue && inStock.Value == 1; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error en IsInUseAsync para Tipo de Bobina ID: {IdTipoBobina}", id); + return true; // Asumir en uso si hay error + } + } + + // --- Métodos de Escritura (USAN TRANSACCIÓN) --- + + public async Task CreateAsync(TipoBobina nuevoTipoBobina, int idUsuario, IDbTransaction transaction) + { + const string sqlInsert = @" + INSERT INTO dbo.bob_dtBobinas (Denominacion) + OUTPUT INSERTED.Id_TipoBobina AS IdTipoBobina, INSERTED.Denominacion + VALUES (@Denominacion);"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_dtBobinas_H (Id_TipoBobina, Denominacion, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdTipoBobina, @Denominacion, @IdUsuario, @FechaMod, @TipoMod);"; + + var connection = transaction.Connection!; + + var insertedTipoBobina = await connection.QuerySingleAsync( + sqlInsert, + new { nuevoTipoBobina.Denominacion }, + transaction: transaction + ); + + if (insertedTipoBobina == null || insertedTipoBobina.IdTipoBobina <= 0) + { + throw new DataException("No se pudo obtener el ID del tipo de bobina insertado."); + } + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdTipoBobina = insertedTipoBobina.IdTipoBobina, + insertedTipoBobina.Denominacion, + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Insertada" + }, transaction: transaction); + + return insertedTipoBobina; + } + + public async Task UpdateAsync(TipoBobina tipoBobinaAActualizar, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var tipoBobinaActual = await connection.QuerySingleOrDefaultAsync( + "SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE Id_TipoBobina = @Id", + new { Id = tipoBobinaAActualizar.IdTipoBobina }, + transaction); + + if (tipoBobinaActual == null) throw new KeyNotFoundException($"No se encontró el tipo de bobina con ID {tipoBobinaAActualizar.IdTipoBobina} para actualizar."); + + const string sqlUpdate = "UPDATE dbo.bob_dtBobinas SET Denominacion = @Denominacion WHERE Id_TipoBobina = @IdTipoBobina;"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_dtBobinas_H (Id_TipoBobina, Denominacion, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdTipoBobina, @DenominacionActual, @IdUsuario, @FechaMod, @TipoMod);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdTipoBobina = tipoBobinaActual.IdTipoBobina, + DenominacionActual = tipoBobinaActual.Denominacion, // Valor ANTES de actualizar + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Modificada" + }, transaction: transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new + { + tipoBobinaAActualizar.Denominacion, + tipoBobinaAActualizar.IdTipoBobina + }, transaction: transaction); + + return rowsAffected == 1; + } + + public async Task DeleteAsync(int id, int idUsuario, IDbTransaction transaction) + { + var connection = transaction.Connection!; + var tipoBobinaActual = await connection.QuerySingleOrDefaultAsync( + "SELECT Id_TipoBobina AS IdTipoBobina, Denominacion FROM dbo.bob_dtBobinas WHERE Id_TipoBobina = @Id", + new { Id = id }, + transaction); + + if (tipoBobinaActual == null) throw new KeyNotFoundException($"No se encontró el tipo de bobina con ID {id} para eliminar."); + + const string sqlDelete = "DELETE FROM dbo.bob_dtBobinas WHERE Id_TipoBobina = @Id"; + const string sqlInsertHistorico = @" + INSERT INTO dbo.bob_dtBobinas_H (Id_TipoBobina, Denominacion, Id_Usuario, FechaMod, TipoMod) + VALUES (@IdTipoBobina, @Denominacion, @IdUsuario, @FechaMod, @TipoMod);"; + + await connection.ExecuteAsync(sqlInsertHistorico, new + { + IdTipoBobina = tipoBobinaActual.IdTipoBobina, + tipoBobinaActual.Denominacion, // Valor ANTES de borrar + IdUsuario = idUsuario, + FechaMod = DateTime.Now, + TipoMod = "Eliminada" + }, transaction: transaction); + + var rowsAffected = await connection.ExecuteAsync(sqlDelete, new { Id = id }, transaction: transaction); + + return rowsAffected == 1; + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/Empresa.cs b/Backend/GestionIntegral.Api/Models/Distribucion/Empresa.cs new file mode 100644 index 0000000..f774402 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/Empresa.cs @@ -0,0 +1,9 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class Empresa + { + public int IdEmpresa { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/Zona.cs b/Backend/GestionIntegral.Api/Models/Distribucion/Zona.cs new file mode 100644 index 0000000..af973a1 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/Zona.cs @@ -0,0 +1,10 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class Zona + { + public int IdZona { get; set; } // Coincide con PK de dist_dtZonas + public string Nombre { get; set; } = string.Empty; + public string? Descripcion { get; set; } + public bool Estado { get; set; } // Para el soft delete + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Distribucion/ZonaHistorico.cs b/Backend/GestionIntegral.Api/Models/Distribucion/ZonaHistorico.cs new file mode 100644 index 0000000..20bb2f5 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Distribucion/ZonaHistorico.cs @@ -0,0 +1,13 @@ +namespace GestionIntegral.Api.Models.Distribucion +{ + public class ZonaHistorico + { + public int IdZona { get; set; } // NO es IDENTITY + public string Nombre { get; set; } = string.Empty; + public string? Descripcion { get; set; } + public bool Estado { get; set; } // Importante para registrar el estado al momento del cambio + public int IdUsuario { 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/Dtos/Empresas/CreateEmpresaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Empresas/CreateEmpresaDto.cs new file mode 100644 index 0000000..c7a9f41 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Empresas/CreateEmpresaDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Empresas +{ + public class CreateEmpresaDto + { + [Required(ErrorMessage = "El nombre de la empresa es obligatorio.")] + [StringLength(100)] + public string Nombre { get; set; } = string.Empty; + [StringLength(250)] + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Empresas/EmpresaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Empresas/EmpresaDto.cs new file mode 100644 index 0000000..0f22d70 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Empresas/EmpresaDto.cs @@ -0,0 +1,9 @@ +namespace GestionIntegral.Api.Dtos.Empresas +{ + public class EmpresaDto + { + public int IdEmpresa { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Empresas/UpdateEmpresaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Empresas/UpdateEmpresaDto.cs new file mode 100644 index 0000000..6bb9ac4 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Empresas/UpdateEmpresaDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; +namespace GestionIntegral.Api.Dtos.Empresas +{ + public class UpdateEmpresaDto + { + [Required(ErrorMessage = "El nombre de la empresa es obligatorio.")] + [StringLength(100)] + public string Nombre { get; set; } = string.Empty; + [StringLength(250)] + public string? Detalle { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateEstadoBobinaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateEstadoBobinaDto.cs new file mode 100644 index 0000000..63733d8 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateEstadoBobinaDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class CreateEstadoBobinaDto + { + [Required(ErrorMessage = "La denominación del estado de bobina es obligatoria.")] + [StringLength(50, ErrorMessage = "La denominación no puede exceder los 50 caracteres.")] + public string Denominacion { get; set; } = string.Empty; + + [StringLength(150, ErrorMessage = "La observación no puede exceder los 150 caracteres.")] + public string? Obs { get; set; } // Observación es opcional + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreatePlantaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreatePlantaDto.cs new file mode 100644 index 0000000..faa46e0 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreatePlantaDto.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class CreatePlantaDto + { + [Required(ErrorMessage = "El nombre de la planta es obligatorio.")] + [StringLength(50, ErrorMessage = "El nombre no puede exceder los 50 caracteres.")] + public string Nombre { get; set; } = string.Empty; + + [Required(ErrorMessage = "El detalle de la planta es obligatorio.")] // Basado en que la tabla no permite NULL + [StringLength(200, ErrorMessage = "El detalle no puede exceder los 200 caracteres.")] + public string Detalle { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateTipoBobinaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateTipoBobinaDto.cs new file mode 100644 index 0000000..441ac28 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateTipoBobinaDto.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class CreateTipoBobinaDto + { + [Required(ErrorMessage = "La denominación del tipo de bobina es obligatoria.")] + [StringLength(150, ErrorMessage = "La denominación no puede exceder los 150 caracteres.")] + public string Denominacion { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/EstadoBobinaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/EstadoBobinaDto.cs new file mode 100644 index 0000000..5b92ea4 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/EstadoBobinaDto.cs @@ -0,0 +1,9 @@ +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class EstadoBobinaDto + { + public int IdEstadoBobina { get; set; } + public string Denominacion { get; set; } = string.Empty; + public string? Obs { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/PlantaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/PlantaDto.cs new file mode 100644 index 0000000..cc63f1e --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/PlantaDto.cs @@ -0,0 +1,9 @@ +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class PlantaDto + { + public int IdPlanta { get; set; } + public string Nombre { get; set; } = string.Empty; + public string Detalle { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/TipoBobinaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/TipoBobinaDto.cs new file mode 100644 index 0000000..a1dd392 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/TipoBobinaDto.cs @@ -0,0 +1,8 @@ +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class TipoBobinaDto + { + public int IdTipoBobina { get; set; } + public string Denominacion { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateEstadoBobinaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateEstadoBobinaDto.cs new file mode 100644 index 0000000..53e7526 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateEstadoBobinaDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class UpdateEstadoBobinaDto + { + [Required(ErrorMessage = "La denominación del estado de bobina es obligatoria.")] + [StringLength(50, ErrorMessage = "La denominación no puede exceder los 50 caracteres.")] + public string Denominacion { get; set; } = string.Empty; + + [StringLength(150, ErrorMessage = "La observación no puede exceder los 150 caracteres.")] + public string? Obs { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdatePlantaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdatePlantaDto.cs new file mode 100644 index 0000000..f02964f --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdatePlantaDto.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class UpdatePlantaDto + { + [Required(ErrorMessage = "El nombre de la planta es obligatorio.")] + [StringLength(50, ErrorMessage = "El nombre no puede exceder los 50 caracteres.")] + public string Nombre { get; set; } = string.Empty; + + [Required(ErrorMessage = "El detalle de la planta es obligatorio.")] + [StringLength(200, ErrorMessage = "El detalle no puede exceder los 200 caracteres.")] + public string Detalle { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateTipoBobinaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateTipoBobinaDto.cs new file mode 100644 index 0000000..cefff1e --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateTipoBobinaDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Impresion +{ + public class UpdateTipoBobinaDto + { + // El ID se pasa por la ruta, no en el body para PUT + [Required(ErrorMessage = "La denominación del tipo de bobina es obligatoria.")] + [StringLength(150, ErrorMessage = "La denominación no puede exceder los 150 caracteres.")] + public string Denominacion { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Zonas/CreateZonaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Zonas/CreateZonaDto.cs new file mode 100644 index 0000000..0e4853a --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Zonas/CreateZonaDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Zonas +{ + public class CreateZonaDto + { + [Required(ErrorMessage = "El nombre de la zona es obligatorio.")] + [StringLength(50, ErrorMessage = "El nombre no puede exceder los 50 caracteres.")] + public string Nombre { get; set; } = string.Empty; + + [StringLength(150, ErrorMessage = "La descripción no puede exceder los 150 caracteres.")] + public string? Descripcion { get; set; } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Zonas/UpdateZonaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Zonas/UpdateZonaDto.cs new file mode 100644 index 0000000..16af8e7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Zonas/UpdateZonaDto.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace GestionIntegral.Api.Dtos.Zonas +{ + public class UpdateZonaDto + { + [Required(ErrorMessage = "El nombre de la zona es obligatorio.")] + [StringLength(50, ErrorMessage = "El nombre no puede exceder los 50 caracteres.")] + public string Nombre { get; set; } = string.Empty; + + [StringLength(150, ErrorMessage = "La descripción no puede exceder los 150 caracteres.")] + public string? Descripcion { get; set; } + // No incluimos Estado aquí, ya que el borrado lógico es una operación separada (DELETE) + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Dtos/Zonas/ZonaDto.cs b/Backend/GestionIntegral.Api/Models/Dtos/Zonas/ZonaDto.cs new file mode 100644 index 0000000..3e03dd4 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Dtos/Zonas/ZonaDto.cs @@ -0,0 +1,10 @@ +namespace GestionIntegral.Api.Dtos.Zonas +{ + public class ZonaDto + { + public int IdZona { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Descripcion { get; set; } + // No incluimos 'Estado' porque generalmente solo mostraremos las activas + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Empresas/EmpresaHistorico.cs b/Backend/GestionIntegral.Api/Models/Empresas/EmpresaHistorico.cs new file mode 100644 index 0000000..fe5e7b7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Empresas/EmpresaHistorico.cs @@ -0,0 +1,12 @@ +namespace GestionIntegral.Api.Models.Empresas +{ + public class EmpresaHistorico + { + public int IdEmpresa { get; set; } + public string Nombre { get; set; } = string.Empty; + public string? Detalle { get; set; } + public int IdUsuario { 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/Impresion/EstadoBobina.cs b/Backend/GestionIntegral.Api/Models/Impresion/EstadoBobina.cs new file mode 100644 index 0000000..36158b7 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/EstadoBobina.cs @@ -0,0 +1,14 @@ +namespace GestionIntegral.Api.Models.Impresion +{ + public class EstadoBobina + { + // Columna: Id_EstadoBobina (PK, Identity) + public int IdEstadoBobina { get; set; } + + // Columna: Denominacion (varchar(50), NOT NULL) + public string Denominacion { get; set; } = string.Empty; + + // Columna: Obs (varchar(150), NULL) + public string? Obs { get; set; } // Permite nulos + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Impresion/EstadoBobinaHistorico.cs b/Backend/GestionIntegral.Api/Models/Impresion/EstadoBobinaHistorico.cs new file mode 100644 index 0000000..f0e3baf --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/EstadoBobinaHistorico.cs @@ -0,0 +1,19 @@ +namespace GestionIntegral.Api.Models.Impresion +{ + public class EstadoBobinaHistorico + { + // Columna: Id_EstadoBobina (int, FK) + public int IdEstadoBobina { get; set; } + + // Columna: Denominacion (varchar(50), NOT NULL) + public string Denominacion { get; set; } = string.Empty; + + // Columna: Obs (varchar(150), NULL) + public string? Obs { get; set; } + + // Columnas de Auditoría + public int IdUsuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; // "Insertada", "Modificada", "Eliminada" + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Impresion/Planta.cs b/Backend/GestionIntegral.Api/Models/Impresion/Planta.cs new file mode 100644 index 0000000..3863207 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/Planta.cs @@ -0,0 +1,9 @@ +namespace GestionIntegral.Api.Models.Impresion +{ + public class Planta + { + public int IdPlanta { get; set; } // Coincide con PK de bob_dtPlantas + public string Nombre { get; set; } = string.Empty; + public string Detalle { get; set; } = string.Empty; // Asumiendo que no es null en BD según script + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Impresion/PlantaHistorico.cs b/Backend/GestionIntegral.Api/Models/Impresion/PlantaHistorico.cs new file mode 100644 index 0000000..16ed802 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/PlantaHistorico.cs @@ -0,0 +1,12 @@ +namespace GestionIntegral.Api.Models.Impresion +{ + public class PlantaHistorico + { + public int IdPlanta { get; set; } // FK a bob_dtPlantas + public string Nombre { get; set; } = string.Empty; + public string Detalle { get; set; } = string.Empty; + public int IdUsuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; // "Insertada", "Modificada", "Eliminada" + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Impresion/TipoBobina.cs b/Backend/GestionIntegral.Api/Models/Impresion/TipoBobina.cs new file mode 100644 index 0000000..76882af --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/TipoBobina.cs @@ -0,0 +1,11 @@ +namespace GestionIntegral.Api.Models.Impresion +{ + public class TipoBobina + { + // Columna: Id_TipoBobina (PK, Identity) + public int IdTipoBobina { get; set; } + + // Columna: Denominacion (varchar(150), NOT NULL) + public string Denominacion { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Models/Impresion/TipoBobinaHistorico.cs b/Backend/GestionIntegral.Api/Models/Impresion/TipoBobinaHistorico.cs new file mode 100644 index 0000000..5f04ed9 --- /dev/null +++ b/Backend/GestionIntegral.Api/Models/Impresion/TipoBobinaHistorico.cs @@ -0,0 +1,16 @@ +namespace GestionIntegral.Api.Models.Impresion +{ + public class TipoBobinaHistorico + { + // Columna: Id_TipoBobina (int, FK) + public int IdTipoBobina { get; set; } + + // Columna: Denominacion (varchar(150), NOT NULL) + public string Denominacion { get; set; } = string.Empty; + + // Columnas de Auditoría + public int IdUsuario { get; set; } + public DateTime FechaMod { get; set; } + public string TipoMod { get; set; } = string.Empty; // "Insertada", "Modificada", "Eliminada" + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Program.cs b/Backend/GestionIntegral.Api/Program.cs index c81e178..3d4ceb8 100644 --- a/Backend/GestionIntegral.Api/Program.cs +++ b/Backend/GestionIntegral.Api/Program.cs @@ -4,7 +4,11 @@ using System.Text; using GestionIntegral.Api.Data; using GestionIntegral.Api.Services; using GestionIntegral.Api.Services.Contables; -using GestionIntegral.Api.Data.Repositories; +using GestionIntegral.Api.Services.Distribucion; +using GestionIntegral.Api.Data.Repositories.Contables; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Data.Repositories.Impresion; +using GestionIntegral.Api.Services.Impresion; var builder = WebApplication.CreateBuilder(args); @@ -15,6 +19,17 @@ 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/TipoPagoService.cs b/Backend/GestionIntegral.Api/Services/Contables/TipoPagoService.cs index 50eeb44..0ab5797 100644 --- a/Backend/GestionIntegral.Api/Services/Contables/TipoPagoService.cs +++ b/Backend/GestionIntegral.Api/Services/Contables/TipoPagoService.cs @@ -1,4 +1,4 @@ -using GestionIntegral.Api.Data.Repositories; // Para ITipoPagoRepository +using GestionIntegral.Api.Data.Repositories.Contables; using GestionIntegral.Api.Dtos.Contables; using GestionIntegral.Api.Models.Contables; // Para TipoPago using GestionIntegral.Api.Services.Contables; diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/EmpresaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/EmpresaService.cs new file mode 100644 index 0000000..fda32f3 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/EmpresaService.cs @@ -0,0 +1,238 @@ +using GestionIntegral.Api.Dtos.Empresas; // Para los DTOs (EmpresaDto, CreateEmpresaDto, UpdateEmpresaDto) +using Microsoft.Extensions.Logging; // Para ILogger +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Data; +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Models.Distribucion; +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Data.Repositories.Contables; // Para IDbTransaction, ConnectionState + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class EmpresaService : IEmpresaService + { + private readonly IEmpresaRepository _empresaRepository; + private readonly ISaldoRepository _saldoRepository; + private readonly DbConnectionFactory _connectionFactory; // Para manejar la transacción + private readonly ILogger _logger; + + public EmpresaService( + IEmpresaRepository empresaRepository, + ISaldoRepository saldoRepository, + DbConnectionFactory connectionFactory, + ILogger logger) + { + _empresaRepository = empresaRepository; + _saldoRepository = saldoRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + public async Task> ObtenerTodasAsync(string? nombreFilter, string? detalleFilter) + { + // El repositorio ya devuelve solo las activas si es necesario + var empresas = await _empresaRepository.GetAllAsync(nombreFilter, detalleFilter); + // Mapeo Entidad -> DTO + return empresas.Select(e => new EmpresaDto + { + IdEmpresa = e.IdEmpresa, + Nombre = e.Nombre, + Detalle = e.Detalle + }); + } + + public async Task ObtenerPorIdAsync(int id) + { + // El repositorio ya devuelve solo las activas si es necesario + var empresa = await _empresaRepository.GetByIdAsync(id); + if (empresa == null) return null; + // Mapeo Entidad -> DTO + return new EmpresaDto + { + IdEmpresa = empresa.IdEmpresa, + Nombre = empresa.Nombre, + Detalle = empresa.Detalle + }; + } + + public async Task<(EmpresaDto? Empresa, string? Error)> CrearAsync(CreateEmpresaDto createDto, int idUsuario) + { + // Validación de negocio: Nombre duplicado + if (await _empresaRepository.ExistsByNameAsync(createDto.Nombre)) + { + return (null, "El nombre de la empresa ya existe."); + } + + var nuevaEmpresa = new Empresa + { + Nombre = createDto.Nombre, + Detalle = createDto.Detalle + }; + + // --- Transacción --- + using (var connection = _connectionFactory.CreateConnection()) + { + // Abrir conexión de forma asíncrona + if (connection is System.Data.Common.DbConnection dbConnection) + { + await dbConnection.OpenAsync(); + } + else + { + connection.Open(); // Fallback síncrono + } + + using (var transaction = connection.BeginTransaction()) + { + try + { + // 1. Crear Empresa (Repo maneja su historial dentro de esta transacción) + var empresaCreada = await _empresaRepository.CreateAsync(nuevaEmpresa, idUsuario, transaction); + if (empresaCreada == null) + { + throw new InvalidOperationException("No se pudo crear la empresa en el repositorio."); + } + + // 2. Obtener IDs de Distribuidores + var distribuidoresIds = await _saldoRepository.GetAllDistribuidorIdsAsync(); // No necesita transacción si solo lee + + // 3. Crear Saldos Iniciales (CERO) para cada distribuidor en esta nueva empresa + foreach (var idDistribuidor in distribuidoresIds) + { + bool saldoCreado = await _saldoRepository.CreateSaldoInicialAsync("Distribuidores", idDistribuidor, empresaCreada.IdEmpresa, transaction); + if (!saldoCreado) + { + throw new InvalidOperationException($"Falló al crear saldo inicial para distribuidor {idDistribuidor} y nueva empresa {empresaCreada.IdEmpresa}."); + } + _logger.LogInformation("Saldo inicial creado para Distribuidor ID {IdDistribuidor}, Empresa ID {IdEmpresa}", idDistribuidor, empresaCreada.IdEmpresa); + } + + transaction.Commit(); + + var empresaDto = new EmpresaDto + { + IdEmpresa = empresaCreada.IdEmpresa, + Nombre = empresaCreada.Nombre, + Detalle = empresaCreada.Detalle + }; + _logger.LogInformation("Empresa ID {IdEmpresa} creada exitosamente por Usuario ID {IdUsuario}.", empresaCreada.IdEmpresa, idUsuario); + return (empresaDto, null); // Éxito + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error al intentar hacer rollback en CrearAsync Empresa."); } + _logger.LogError(ex, "Error en transacción CrearAsync para Empresa. Nombre: {Nombre}", createDto.Nombre); + return (null, "Error interno al procesar la creación de la empresa."); + } + } // La transacción se dispone aquí (y cierra la conexión si no hubo commit/rollback explícito, aunque ya lo hacemos) + } // La conexión se cierra/dispone aquí + // --- Fin Transacción --- + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateEmpresaDto updateDto, int idUsuario) + { + var empresaExistente = await _empresaRepository.GetByIdAsync(id); + if (empresaExistente == null) + { + return (false, "Empresa no encontrada."); + } + + if (await _empresaRepository.ExistsByNameAsync(updateDto.Nombre, id)) + { + return (false, "El nombre de la empresa ya existe para otro registro."); + } + + empresaExistente.Nombre = updateDto.Nombre; + empresaExistente.Detalle = updateDto.Detalle; + + // --- Transacción --- + using (var connection = _connectionFactory.CreateConnection()) + { + if (connection is System.Data.Common.DbConnection dbConnection) { await dbConnection.OpenAsync(); } else { connection.Open(); } + using (var transaction = connection.BeginTransaction()) + { + try + { + var actualizado = await _empresaRepository.UpdateAsync(empresaExistente, idUsuario, transaction); + if (!actualizado) + { + throw new InvalidOperationException("La actualización en el repositorio de empresas devolvió false."); + } + transaction.Commit(); + _logger.LogInformation("Empresa ID {IdEmpresa} actualizada exitosamente por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); // Éxito + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error al intentar hacer rollback en ActualizarAsync Empresa."); } + _logger.LogError(ex, "Error en transacción ActualizarAsync para Empresa ID: {Id}", id); + return (false, "Error interno al actualizar la empresa."); + } + } + } + // --- Fin Transacción --- + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) + { + // Primero verificamos si existe, incluso inactiva, para evitar errores 404 si no existe + var empresaExistente = await _empresaRepository.GetByIdAsync(id); + if (empresaExistente == null) + { + return (false, "Empresa no encontrada."); + } + + // Validación: ¿Está en uso? + if (await _empresaRepository.IsInUseAsync(id)) + { + return (false, "No se puede eliminar. Existen publicaciones relacionadas a la empresa."); + } + + // --- Transacción --- + using (var connection = _connectionFactory.CreateConnection()) + { + if (connection is System.Data.Common.DbConnection dbConnection) { await dbConnection.OpenAsync(); } else { connection.Open(); } + using (var transaction = connection.BeginTransaction()) + { + try + { + // 1. Eliminar Saldos asociados + bool saldosEliminados = await _saldoRepository.DeleteSaldosByEmpresaAsync(id, transaction); + // No lanzamos error si saldosEliminados es false, podría no haber tenido saldos. Loggeamos si es necesario. + if (!saldosEliminados && await _saldoRepository.CheckIfSaldosExistForEmpresaAsync(id)) // Necesitarías este método en ISaldoRepository + { + _logger.LogWarning("Se intentó eliminar Empresa ID {IdEmpresa} pero falló la eliminación de saldos asociados.", id); + // Decidir si continuar o fallar. Por ahora, continuamos pero loggeamos. + // throw new InvalidOperationException("Error al intentar eliminar los saldos asociados a la empresa."); + } else if (!saldosEliminados) { + _logger.LogInformation("No se encontraron saldos para eliminar de la Empresa ID {IdEmpresa}.", id); + } else { + _logger.LogInformation("Saldos eliminados para Empresa ID {IdEmpresa}.", id); + } + + + // 2. Eliminar Empresa (Repo maneja historial) + var eliminado = await _empresaRepository.DeleteAsync(id, idUsuario, transaction); + if (!eliminado) + { + throw new InvalidOperationException("La eliminación en el repositorio de empresas devolvió false."); + } + + transaction.Commit(); + _logger.LogInformation("Empresa ID {IdEmpresa} eliminada exitosamente por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); // Éxito + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error al intentar hacer rollback en EliminarAsync Empresa."); } + _logger.LogError(ex, "Error en transacción EliminarAsync para Empresa ID: {Id}", id); + return (false, "Error interno al eliminar la empresa."); + } + } + } + // --- Fin Transacción --- + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/IEmpresaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IEmpresaService.cs new file mode 100644 index 0000000..328e235 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IEmpresaService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Empresas; // DTOs de Empresas +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IEmpresaService + { + Task> ObtenerTodasAsync(string? nombreFilter, string? detalleFilter); + Task ObtenerPorIdAsync(int id); + Task<(EmpresaDto? Empresa, string? Error)> CrearAsync(CreateEmpresaDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateEmpresaDto 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/Distribucion/IZonaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/IZonaService.cs new file mode 100644 index 0000000..7e41ab3 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/IZonaService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Zonas; // Para los DTOs de Zonas +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public interface IZonaService + { + Task> ObtenerTodasAsync(string? nombreFilter, string? descripcionFilter); + Task ObtenerPorIdAsync(int id); + Task<(ZonaDto? Zona, string? Error)> CrearAsync(CreateZonaDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateZonaDto updateDto, int idUsuario); + Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario); // Eliminar lógico (cambiar estado) + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Distribucion/ZonaService.cs b/Backend/GestionIntegral.Api/Services/Distribucion/ZonaService.cs new file mode 100644 index 0000000..6be14a3 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Distribucion/ZonaService.cs @@ -0,0 +1,146 @@ +using GestionIntegral.Api.Data.Repositories; // Para IZonaRepository +using GestionIntegral.Api.Data.Repositories.Distribucion; +using GestionIntegral.Api.Dtos.Zonas; +using GestionIntegral.Api.Models.Distribucion; // Para Zona +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Distribucion +{ + public class ZonaService : IZonaService + { + private readonly IZonaRepository _zonaRepository; + private readonly ILogger _logger; + + public ZonaService(IZonaRepository zonaRepository, ILogger logger) + { + _zonaRepository = zonaRepository; + _logger = logger; + } + + public async Task> ObtenerTodasAsync(string? nombreFilter, string? descripcionFilter) + { + var zonas = await _zonaRepository.GetAllAsync(nombreFilter, descripcionFilter, soloActivas: true); // Solo activas por defecto + // Mapeo Entidad -> DTO + return zonas.Select(z => new ZonaDto + { + IdZona = z.IdZona, + Nombre = z.Nombre, + Descripcion = z.Descripcion + }); + } + + public async Task ObtenerPorIdAsync(int id) + { + var zona = await _zonaRepository.GetByIdAsync(id, soloActivas: true); // Solo activa por defecto + if (zona == null) return null; + + // Mapeo Entidad -> DTO + return new ZonaDto + { + IdZona = zona.IdZona, + Nombre = zona.Nombre, + Descripcion = zona.Descripcion + }; + } + + public async Task<(ZonaDto? Zona, string? Error)> CrearAsync(CreateZonaDto createDto, int idUsuario) + { + // Validación: Nombre duplicado (entre zonas activas) + if (await _zonaRepository.ExistsByNameAsync(createDto.Nombre, null, soloActivas: true)) + { + return (null, "El nombre de la zona ya existe."); + } + + var nuevaZona = new Zona + { + Nombre = createDto.Nombre, + Descripcion = createDto.Descripcion, + Estado = true // Las zonas nuevas siempre están activas + }; + + var zonaCreada = await _zonaRepository.CreateAsync(nuevaZona, idUsuario); + + if (zonaCreada == null) + { + _logger.LogError("Falló la creación de la Zona en el repositorio para el nombre: {Nombre}", createDto.Nombre); + return (null, "Error al crear la zona en la base de datos."); + } + + // Mapeo de Entidad creada a DTO + var zonaDto = new ZonaDto + { + IdZona = zonaCreada.IdZona, + Nombre = zonaCreada.Nombre, + Descripcion = zonaCreada.Descripcion + }; + + return (zonaDto, null); // Éxito + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateZonaDto updateDto, int idUsuario) + { + // Verificar si la zona existe y está activa para poder modificarla (regla de negocio) + var zonaExistente = await _zonaRepository.GetByIdAsync(id, soloActivas: true); + if (zonaExistente == null) + { + // Podría no existir o estar inactiva + var zonaInactiva = await _zonaRepository.GetByIdAsync(id, soloActivas: false); + if (zonaInactiva != null) + return (false, "No se puede modificar una zona eliminada (inactiva)."); + else + return (false, "Zona no encontrada."); + } + + // Validación: Nombre duplicado (excluyendo el ID actual, entre zonas activas) + if (await _zonaRepository.ExistsByNameAsync(updateDto.Nombre, id, soloActivas: true)) + { + return (false, "El nombre de la zona ya existe para otro registro activo."); + } + + // Mapeo de DTO a Entidad para actualizar + zonaExistente.Nombre = updateDto.Nombre; + zonaExistente.Descripcion = updateDto.Descripcion; + // No modificamos el estado aquí, solo nombre y descripción + + var actualizado = await _zonaRepository.UpdateAsync(zonaExistente, idUsuario); + + if (!actualizado) + { + _logger.LogError("Falló la actualización de la Zona en el repositorio para el ID: {Id}", id); + return (false, "Error al actualizar la zona en la base de datos."); + } + return (true, null); // Éxito + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) + { + // Verificar si la zona existe (incluso si está inactiva, para evitar errores) + var zonaExistente = await _zonaRepository.GetByIdAsync(id, soloActivas: false); + if (zonaExistente == null) + { + return (false, "Zona no encontrada."); + } + // Si ya está inactiva, consideramos la operación exitosa (idempotencia) + if (!zonaExistente.Estado) { + return (true, null); + } + + // Validación: No se puede eliminar si está en uso por distribuidores o canillas activos + if (await _zonaRepository.IsInUseAsync(id)) + { + return (false, "No se puede eliminar. La zona está asignada a distribuidores o canillas activos."); + } + + // Realizar el Soft Delete + var eliminado = await _zonaRepository.SoftDeleteAsync(id, idUsuario); + if (!eliminado) + { + _logger.LogError("Falló la eliminación lógica de la Zona en el repositorio para el ID: {Id}", id); + return (false, "Error al eliminar la zona de la base de datos."); + } + return (true, null); // Éxito + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Impresion/EstadoBobinaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/EstadoBobinaService.cs new file mode 100644 index 0000000..a08aa34 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Impresion/EstadoBobinaService.cs @@ -0,0 +1,152 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Impresion; +using GestionIntegral.Api.Dtos.Impresion; +using GestionIntegral.Api.Models.Impresion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Impresion +{ + public class EstadoBobinaService : IEstadoBobinaService + { + private readonly IEstadoBobinaRepository _estadoBobinaRepository; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public EstadoBobinaService(IEstadoBobinaRepository estadoBobinaRepository, DbConnectionFactory connectionFactory, ILogger logger) + { + _estadoBobinaRepository = estadoBobinaRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private EstadoBobinaDto MapToDto(EstadoBobina estadoBobina) => new EstadoBobinaDto + { + IdEstadoBobina = estadoBobina.IdEstadoBobina, + Denominacion = estadoBobina.Denominacion, + Obs = estadoBobina.Obs + }; + + public async Task> ObtenerTodosAsync(string? denominacionFilter) + { + var estadosBobina = await _estadoBobinaRepository.GetAllAsync(denominacionFilter); + return estadosBobina.Select(MapToDto); + } + + public async Task ObtenerPorIdAsync(int id) + { + var estadoBobina = await _estadoBobinaRepository.GetByIdAsync(id); + return estadoBobina == null ? null : MapToDto(estadoBobina); + } + + public async Task<(EstadoBobinaDto? EstadoBobina, string? Error)> CrearAsync(CreateEstadoBobinaDto createDto, int idUsuario) + { + if (await _estadoBobinaRepository.ExistsByDenominacionAsync(createDto.Denominacion)) + { + return (null, "La denominación del estado de bobina ya existe."); + } + + var nuevoEstado = new EstadoBobina { Denominacion = createDto.Denominacion, Obs = createDto.Obs }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var estadoCreado = await _estadoBobinaRepository.CreateAsync(nuevoEstado, idUsuario, transaction); + if (estadoCreado == null) throw new DataException("La creación en el repositorio devolvió null."); + + transaction.Commit(); // Síncrono + _logger.LogInformation("EstadoBobina ID {IdEstadoBobina} creado por Usuario ID {IdUsuario}.", estadoCreado.IdEstadoBobina, idUsuario); + return (MapToDto(estadoCreado), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback CrearAsync EstadoBobina."); } + _logger.LogError(ex, "Error CrearAsync EstadoBobina. Denominación: {Denominacion}", createDto.Denominacion); + return (null, $"Error interno al crear el estado de bobina: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateEstadoBobinaDto updateDto, int idUsuario) + { + if (await _estadoBobinaRepository.ExistsByDenominacionAsync(updateDto.Denominacion, id)) + { + return (false, "La denominación del estado de bobina ya existe para otro registro."); + } + + var estadoAActualizar = new EstadoBobina { IdEstadoBobina = id, Denominacion = updateDto.Denominacion, Obs = updateDto.Obs }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var actualizado = await _estadoBobinaRepository.UpdateAsync(estadoAActualizar, idUsuario, transaction); + if (!actualizado) throw new DataException("La operación de actualización no afectó ninguna fila."); + + transaction.Commit(); // Síncrono + _logger.LogInformation("EstadoBobina ID {IdEstadoBobina} actualizado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException knfex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback ActualizarAsync EstadoBobina."); } + _logger.LogWarning(knfex, "Intento de actualizar EstadoBobina ID: {Id} no encontrado.", id); + return (false, "Estado de bobina no encontrado."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback ActualizarAsync EstadoBobina."); } + _logger.LogError(ex, "Error ActualizarAsync EstadoBobina ID: {Id}", id); + return (false, $"Error interno al actualizar el estado de bobina: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) + { + // Estados "fijos" como Disponible (1), En Uso (2), Dañada (3) probablemente no deberían eliminarse. + // Podrías añadir una validación aquí o en el repositorio si es necesario. + if (id <= 3) // Asumiendo IDs fijos para los estados base + { + return (false, "Los estados base (Disponible, En Uso, Dañada) no se pueden eliminar."); + } + + if (await _estadoBobinaRepository.IsInUseAsync(id)) + { + return (false, "No se puede eliminar. El estado de bobina está siendo utilizado en el stock."); + } + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open(); + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var eliminado = await _estadoBobinaRepository.DeleteAsync(id, idUsuario, transaction); + if (!eliminado) throw new DataException("La operación de eliminación no afectó ninguna fila."); + + transaction.Commit(); // Síncrono + _logger.LogInformation("EstadoBobina ID {IdEstadoBobina} eliminado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException knfex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback EliminarAsync EstadoBobina."); } + _logger.LogWarning(knfex, "Intento de eliminar EstadoBobina ID: {Id} no encontrado.", id); + return (false, "Estado de bobina no encontrado."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error rollback EliminarAsync EstadoBobina."); } + _logger.LogError(ex, "Error EliminarAsync EstadoBobina ID: {Id}", id); + return (false, $"Error interno al eliminar el estado de bobina: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Impresion/IEstadoBobinaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/IEstadoBobinaService.cs new file mode 100644 index 0000000..d1a4a16 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Impresion/IEstadoBobinaService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Impresion; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Impresion +{ + public interface IEstadoBobinaService + { + Task> ObtenerTodosAsync(string? denominacionFilter); + Task ObtenerPorIdAsync(int id); + Task<(EstadoBobinaDto? EstadoBobina, string? Error)> CrearAsync(CreateEstadoBobinaDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateEstadoBobinaDto 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/Impresion/IPlantaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/IPlantaService.cs new file mode 100644 index 0000000..6528513 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Impresion/IPlantaService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Impresion; // Para los DTOs +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Impresion +{ + public interface IPlantaService + { + Task> ObtenerTodasAsync(string? nombreFilter, string? detalleFilter); + Task ObtenerPorIdAsync(int id); + Task<(PlantaDto? Planta, string? Error)> CrearAsync(CreatePlantaDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdatePlantaDto 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/Impresion/ITipoBobinaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/ITipoBobinaService.cs new file mode 100644 index 0000000..7291773 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Impresion/ITipoBobinaService.cs @@ -0,0 +1,15 @@ +using GestionIntegral.Api.Dtos.Impresion; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Impresion +{ + public interface ITipoBobinaService + { + Task> ObtenerTodosAsync(string? denominacionFilter); + Task ObtenerPorIdAsync(int id); + Task<(TipoBobinaDto? TipoBobina, string? Error)> CrearAsync(CreateTipoBobinaDto createDto, int idUsuario); + Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateTipoBobinaDto 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/Impresion/PlantaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/PlantaService.cs new file mode 100644 index 0000000..294cc68 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Impresion/PlantaService.cs @@ -0,0 +1,185 @@ +using GestionIntegral.Api.Data; // Para DbConnectionFactory +using GestionIntegral.Api.Data.Repositories.Impresion; +using GestionIntegral.Api.Dtos.Impresion; +using GestionIntegral.Api.Models.Impresion; // Para Planta +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; // Para IsolationLevel +using System.Linq; +using System.Threading.Tasks; // Todavía usamos async para operaciones de BD + +namespace GestionIntegral.Api.Services.Impresion +{ + public class PlantaService : IPlantaService + { + private readonly IPlantaRepository _plantaRepository; + private readonly DbConnectionFactory _connectionFactory; // Para manejar transacciones + private readonly ILogger _logger; + + public PlantaService(IPlantaRepository plantaRepository, DbConnectionFactory connectionFactory, ILogger logger) + { + _plantaRepository = plantaRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + // Método para mapear Planta a PlantaDto + private PlantaDto MapToDto(Planta planta) => new PlantaDto + { + IdPlanta = planta.IdPlanta, + Nombre = planta.Nombre, + Detalle = planta.Detalle + }; + + public async Task> ObtenerTodasAsync(string? nombreFilter, string? detalleFilter) + { + // Las operaciones de lectura no suelen necesitar transacción explícita aquí + var plantas = await _plantaRepository.GetAllAsync(nombreFilter, detalleFilter); + return plantas.Select(MapToDto); + } + + public async Task ObtenerPorIdAsync(int id) + { + var planta = await _plantaRepository.GetByIdAsync(id); + return planta == null ? null : MapToDto(planta); + } + + public async Task<(PlantaDto? Planta, string? Error)> CrearAsync(CreatePlantaDto createDto, int idUsuario) + { + if (await _plantaRepository.ExistsByNameAsync(createDto.Nombre)) + { + return (null, "El nombre de la planta ya existe."); + } + + var nuevaPlanta = new Planta + { + Nombre = createDto.Nombre, + Detalle = createDto.Detalle + }; + + using var connection = _connectionFactory.CreateConnection(); + // Abrir la conexión asíncronamente si es posible + if (connection is System.Data.Common.DbConnection dbConnection) + { + await dbConnection.OpenAsync(); + } + else + { + connection.Open(); // Fallback síncrono + } + + // Empezar transacción + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var plantaCreada = await _plantaRepository.CreateAsync(nuevaPlanta, idUsuario, transaction); + if (plantaCreada == null) + { + throw new DataException("La creación en el repositorio devolvió null."); + } + + transaction.Commit(); // <--- CORREGIDO: Commit síncrono + _logger.LogInformation("Planta ID {IdPlanta} creada exitosamente por Usuario ID {IdUsuario}.", plantaCreada.IdPlanta, idUsuario); + return (MapToDto(plantaCreada), null); + } + catch (Exception ex) + { + try + { + transaction.Rollback(); // <--- CORREGIDO: Rollback síncrono + } + catch (Exception rbEx) + { + _logger.LogError(rbEx, "Error adicional durante el rollback en CrearAsync Planta."); + } + _logger.LogError(ex, "Error en transacción CrearAsync para Planta. Nombre: {Nombre}", createDto.Nombre); + return (null, $"Error interno al crear la planta: {ex.Message}"); // Devolver mensaje de error + } + // La conexión y transacción se disponen automáticamente al salir del 'using' + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdatePlantaDto updateDto, int idUsuario) + { + if (await _plantaRepository.ExistsByNameAsync(updateDto.Nombre, id)) + { + return (false, "El nombre de la planta ya existe para otro registro."); + } + + var plantaAActualizar = new Planta + { + IdPlanta = id, + Nombre = updateDto.Nombre, + Detalle = updateDto.Detalle + }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConnection) { await dbConnection.OpenAsync(); } else { connection.Open(); } + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var actualizado = await _plantaRepository.UpdateAsync(plantaAActualizar, idUsuario, transaction); + if (!actualizado) + { + // La excepción KeyNotFoundException se manejará en el bloque catch + // Aquí asumimos que si devuelve false es porque no afectó filas, lo cual podría ser un error lógico + throw new DataException("La operación de actualización no afectó ninguna fila."); + } + + transaction.Commit(); // <--- CORREGIDO: Commit síncrono + _logger.LogInformation("Planta ID {IdPlanta} actualizada exitosamente por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException knfex) // Captura específica si el repo la lanza + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error adicional durante el rollback en ActualizarAsync Planta."); } + _logger.LogWarning(knfex, "Intento de actualizar Planta ID: {Id} no encontrada.", id); + return (false, "Planta no encontrada."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error adicional durante el rollback en ActualizarAsync Planta."); } + _logger.LogError(ex, "Error en transacción ActualizarAsync para Planta ID: {Id}", id); + return (false, $"Error interno al actualizar la planta: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) + { + if (await _plantaRepository.IsInUseAsync(id)) + { + return (false, "No se puede eliminar. La planta está siendo utilizada."); + } + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConnection) { await dbConnection.OpenAsync(); } else { connection.Open(); } + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var eliminado = await _plantaRepository.DeleteAsync(id, idUsuario, transaction); + if (!eliminado) + { + throw new DataException("La operación de eliminación no afectó ninguna fila."); + } + + transaction.Commit(); // <--- CORREGIDO: Commit síncrono + _logger.LogInformation("Planta ID {IdPlanta} eliminada exitosamente por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException knfex) // Captura específica si el repo la lanza + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error adicional durante el rollback en EliminarAsync Planta."); } + _logger.LogWarning(knfex, "Intento de eliminar Planta ID: {Id} no encontrada.", id); + return (false, "Planta no encontrada."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error adicional durante el rollback en EliminarAsync Planta."); } + _logger.LogError(ex, "Error en transacción EliminarAsync para Planta ID: {Id}", id); + return (false, $"Error interno al eliminar la planta: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Backend/GestionIntegral.Api/Services/Impresion/TipoBobinaService.cs b/Backend/GestionIntegral.Api/Services/Impresion/TipoBobinaService.cs new file mode 100644 index 0000000..fa51e35 --- /dev/null +++ b/Backend/GestionIntegral.Api/Services/Impresion/TipoBobinaService.cs @@ -0,0 +1,144 @@ +using GestionIntegral.Api.Data; +using GestionIntegral.Api.Data.Repositories.Impresion; +using GestionIntegral.Api.Dtos.Impresion; +using GestionIntegral.Api.Models.Impresion; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace GestionIntegral.Api.Services.Impresion +{ + public class TipoBobinaService : ITipoBobinaService + { + private readonly ITipoBobinaRepository _tipoBobinaRepository; + private readonly DbConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public TipoBobinaService(ITipoBobinaRepository tipoBobinaRepository, DbConnectionFactory connectionFactory, ILogger logger) + { + _tipoBobinaRepository = tipoBobinaRepository; + _connectionFactory = connectionFactory; + _logger = logger; + } + + private TipoBobinaDto MapToDto(TipoBobina tipoBobina) => new TipoBobinaDto + { + IdTipoBobina = tipoBobina.IdTipoBobina, + Denominacion = tipoBobina.Denominacion + }; + + public async Task> ObtenerTodosAsync(string? denominacionFilter) + { + var tiposBobina = await _tipoBobinaRepository.GetAllAsync(denominacionFilter); + return tiposBobina.Select(MapToDto); + } + + public async Task ObtenerPorIdAsync(int id) + { + var tipoBobina = await _tipoBobinaRepository.GetByIdAsync(id); + return tipoBobina == null ? null : MapToDto(tipoBobina); + } + + public async Task<(TipoBobinaDto? TipoBobina, string? Error)> CrearAsync(CreateTipoBobinaDto createDto, int idUsuario) + { + if (await _tipoBobinaRepository.ExistsByDenominacionAsync(createDto.Denominacion)) + { + return (null, "La denominación del tipo de bobina ya existe."); + } + + var nuevoTipoBobina = new TipoBobina { Denominacion = createDto.Denominacion }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConnection) { await dbConnection.OpenAsync(); } else { connection.Open(); } + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var tipoBobinaCreado = await _tipoBobinaRepository.CreateAsync(nuevoTipoBobina, idUsuario, transaction); + if (tipoBobinaCreado == null) throw new DataException("La creación en el repositorio devolvió null."); + + transaction.Commit(); // Síncrono + _logger.LogInformation("TipoBobina ID {IdTipoBobina} creado por Usuario ID {IdUsuario}.", tipoBobinaCreado.IdTipoBobina, idUsuario); + return (MapToDto(tipoBobinaCreado), null); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error adicional durante el rollback en CrearAsync TipoBobina."); } + _logger.LogError(ex, "Error en transacción CrearAsync para TipoBobina. Denominación: {Denominacion}", createDto.Denominacion); + return (null, $"Error interno al crear el tipo de bobina: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateTipoBobinaDto updateDto, int idUsuario) + { + if (await _tipoBobinaRepository.ExistsByDenominacionAsync(updateDto.Denominacion, id)) + { + return (false, "La denominación del tipo de bobina ya existe para otro registro."); + } + + var tipoBobinaAActualizar = new TipoBobina { IdTipoBobina = id, Denominacion = updateDto.Denominacion }; + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConnection) { await dbConnection.OpenAsync(); } else { connection.Open(); } + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var actualizado = await _tipoBobinaRepository.UpdateAsync(tipoBobinaAActualizar, idUsuario, transaction); + if (!actualizado) throw new DataException("La operación de actualización no afectó ninguna fila."); + + transaction.Commit(); // Síncrono + _logger.LogInformation("TipoBobina ID {IdTipoBobina} actualizado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException knfex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error adicional durante el rollback en ActualizarAsync TipoBobina."); } + _logger.LogWarning(knfex, "Intento de actualizar TipoBobina ID: {Id} no encontrado.", id); + return (false, "Tipo de bobina no encontrado."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error adicional durante el rollback en ActualizarAsync TipoBobina."); } + _logger.LogError(ex, "Error en transacción ActualizarAsync para TipoBobina ID: {Id}", id); + return (false, $"Error interno al actualizar el tipo de bobina: {ex.Message}"); + } + } + + public async Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario) + { + if (await _tipoBobinaRepository.IsInUseAsync(id)) + { + return (false, "No se puede eliminar. El tipo de bobina está siendo utilizado en el stock."); + } + + using var connection = _connectionFactory.CreateConnection(); + if (connection is System.Data.Common.DbConnection dbConnection) { await dbConnection.OpenAsync(); } else { connection.Open(); } + using var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + var eliminado = await _tipoBobinaRepository.DeleteAsync(id, idUsuario, transaction); + if (!eliminado) throw new DataException("La operación de eliminación no afectó ninguna fila."); + + transaction.Commit(); // Síncrono + _logger.LogInformation("TipoBobina ID {IdTipoBobina} eliminado por Usuario ID {IdUsuario}.", id, idUsuario); + return (true, null); + } + catch (KeyNotFoundException knfex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error adicional durante el rollback en EliminarAsync TipoBobina."); } + _logger.LogWarning(knfex, "Intento de eliminar TipoBobina ID: {Id} no encontrado.", id); + return (false, "Tipo de bobina no encontrado."); + } + catch (Exception ex) + { + try { transaction.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error adicional durante el rollback en EliminarAsync TipoBobina."); } + _logger.LogError(ex, "Error en transacción EliminarAsync para TipoBobina ID: {Id}", id); + return (false, $"Error interno al eliminar el tipo de bobina: {ex.Message}"); + } + } + } +} \ No newline at end of file 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 e9e6822..f55c5a4 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+da7b544372d72fd6e4a82a3d95626e9cd273f4a4")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5c4b961073f79c4d61e3b3664349cf440b6ae4f4")] [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.GeneratedMSBuildEditorConfig.editorconfig b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.GeneratedMSBuildEditorConfig.editorconfig index 422e414..87011e3 100644 --- a/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.GeneratedMSBuildEditorConfig.editorconfig +++ b/Backend/GestionIntegral.Api/obj/Debug/net9.0/GestionIntegral.Api.GeneratedMSBuildEditorConfig.editorconfig @@ -9,13 +9,13 @@ build_property.EnforceExtendedAnalyzerRules = build_property._SupportedPlatformList = Linux,macOS,Windows build_property.RootNamespace = GestionIntegral.Api build_property.RootNamespace = GestionIntegral.Api -build_property.ProjectDir = E:\GestionIntegralWeb\backend\gestionintegral.api\ +build_property.ProjectDir = E:\GestionIntegralWeb\Backend\GestionIntegral.Api\ build_property.EnableComHosting = build_property.EnableGeneratedComInterfaceComImportInterop = build_property.RazorLangVersion = 9.0 build_property.SupportLocalizedComponentNames = build_property.GenerateRazorMetadataSourceChecksumAttributes = -build_property.MSBuildProjectDirectory = E:\GestionIntegralWeb\backend\gestionintegral.api +build_property.MSBuildProjectDirectory = E:\GestionIntegralWeb\Backend\GestionIntegral.Api build_property._RazorSourceGeneratorDebug = build_property.EffectiveAnalysisLevelStyle = 9.0 build_property.EnableCodeStyleSeverity = diff --git a/Frontend/src/assets/eldia.png b/Frontend/src/assets/eldia.png new file mode 100644 index 0000000..d86ffbb Binary files /dev/null and b/Frontend/src/assets/eldia.png differ diff --git a/Frontend/src/components/Modals/EmpresaFormModal.tsx b/Frontend/src/components/Modals/EmpresaFormModal.tsx new file mode 100644 index 0000000..7de03dd --- /dev/null +++ b/Frontend/src/components/Modals/EmpresaFormModal.tsx @@ -0,0 +1,142 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; +import type { EmpresaDto } from '../../models/dtos/Empresas/EmpresaDto'; +import type { CreateEmpresaDto } from '../../models/dtos/Empresas/CreateEmpresaDto'; +import type { UpdateEmpresaDto } from '../../models/dtos/Empresas/UpdateEmpresaDto'; // Necesitamos Update DTO también + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +interface EmpresaFormModalProps { + open: boolean; + onClose: () => void; + // El tipo de dato enviado depende si es creación o edición + onSubmit: (data: CreateEmpresaDto | (UpdateEmpresaDto & { idEmpresa: number })) => Promise; + initialData?: EmpresaDto | null; // Para editar + errorMessage?: string | null; // Error de la API pasado desde el padre + clearErrorMessage: () => void; // Función para limpiar el error en el padre +} + +const EmpresaFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [nombre, setNombre] = useState(''); + const [detalle, setDetalle] = useState(''); + const [loading, setLoading] = useState(false); + const [localError, setLocalError] = useState(null); // Para validaciones locales (ej: campo requerido) + + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + // Poblar formulario si es edición, limpiar si es creación + setNombre(initialData?.nombre || ''); + setDetalle(initialData?.detalle || ''); + setLocalError(null); // Limpiar error local al abrir/cambiar + clearErrorMessage(); // Limpiar error API del padre + } + // No necesitamos limpiar al cerrar, ya que el useEffect se ejecuta al abrir `open = true` + }, [open, initialData, clearErrorMessage]); + + // Limpia errores cuando el usuario empieza a escribir + const handleInputChange = () => { + if (localError) setLocalError(null); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setLocalError(null); + clearErrorMessage(); // Limpiar errores antes de enviar + + // Validación local simple + if (!nombre.trim()) { + setLocalError('El nombre es obligatorio.'); + return; + } + + setLoading(true); + try { + // Preparar datos según sea creación o edición + const dataToSubmit = { nombre, detalle: detalle || undefined }; // Usa undefined si detalle está vacío + + if (isEditing && initialData) { + // Si es edición, combina los datos del formulario con el ID existente + await onSubmit({ ...dataToSubmit, idEmpresa: initialData.idEmpresa }); + } else { + // Si es creación, envía solo los datos del formulario + await onSubmit(dataToSubmit as CreateEmpresaDto); + } + onClose(); // Cierra el modal SOLO si onSubmit fue exitoso + } catch (error: any) { + // El error de la API será manejado por el componente padre y mostrado vía 'errorMessage' + // No necesitamos setear localError aquí a menos que sea un error específico del submit no cubierto por la API + console.error("Error en submit de EmpresaFormModal:", error); + // El componente padre es responsable de poner setLoading(false) si onSubmit falla + } finally { + // Asegúrate de que el loading se detenga incluso si hay un error no capturado por el padre + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Empresa' : 'Agregar Nueva Empresa'} + + + {setNombre(e.target.value); handleInputChange();} } + margin="normal" + error={!!localError} // Mostrar error si localError tiene un mensaje + helperText={localError ? localError : ''} // Mostrar el mensaje de error local + disabled={loading} + autoFocus // Poner el foco en el primer campo + /> + {setDetalle(e.target.value); handleInputChange();}} + margin="normal" + multiline + rows={3} + disabled={loading} + /> + {/* Mostrar error de la API si existe */} + {errorMessage && {errorMessage}} + + + + + + + + + ); +}; + +export default EmpresaFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/EstadoBobinaFormModal.tsx b/Frontend/src/components/Modals/EstadoBobinaFormModal.tsx new file mode 100644 index 0000000..bb9018f --- /dev/null +++ b/Frontend/src/components/Modals/EstadoBobinaFormModal.tsx @@ -0,0 +1,128 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; +import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; +import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto'; +import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto'; + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +interface EstadoBobinaFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateEstadoBobinaDto | (UpdateEstadoBobinaDto & { idEstadoBobina: number })) => Promise; + initialData?: EstadoBobinaDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const EstadoBobinaFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [denominacion, setDenominacion] = useState(''); + const [obs, setObs] = useState(''); // Observación + const [loading, setLoading] = useState(false); + const [localErrorDenominacion, setLocalErrorDenominacion] = useState(null); + + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + setDenominacion(initialData?.denominacion || ''); + setObs(initialData?.obs || ''); // Inicializar obs + setLocalErrorDenominacion(null); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const handleInputChange = () => { + if (localErrorDenominacion) setLocalErrorDenominacion(null); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setLocalErrorDenominacion(null); + clearErrorMessage(); + + if (!denominacion.trim()) { + setLocalErrorDenominacion('La denominación es obligatoria.'); + return; + } + + setLoading(true); + try { + const dataToSubmit = { denominacion, obs: obs || undefined }; // Enviar obs si tiene valor + + if (isEditing && initialData) { + await onSubmit({ ...dataToSubmit, idEstadoBobina: initialData.idEstadoBobina }); + } else { + await onSubmit(dataToSubmit as CreateEstadoBobinaDto); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de EstadoBobinaFormModal:", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Estado de Bobina' : 'Agregar Nuevo Estado de Bobina'} + + + { setDenominacion(e.target.value); handleInputChange(); }} + margin="normal" + error={!!localErrorDenominacion} + helperText={localErrorDenominacion || ''} + disabled={loading} + autoFocus + /> + setObs(e.target.value)} // No necesita handleInputChange si no tiene validación local + margin="normal" + multiline + rows={3} + disabled={loading} + /> + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default EstadoBobinaFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/PlantaFormModal.tsx b/Frontend/src/components/Modals/PlantaFormModal.tsx new file mode 100644 index 0000000..fc8c37f --- /dev/null +++ b/Frontend/src/components/Modals/PlantaFormModal.tsx @@ -0,0 +1,144 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; +import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; +import type { CreatePlantaDto } from '../../models/dtos/Impresion/CreatePlantaDto'; +import type { UpdatePlantaDto } from '../../models/dtos/Impresion/UpdatePlantaDto'; + +const modalStyle = { /* ... (mismo estilo que otros modales) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +interface PlantaFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreatePlantaDto | (UpdatePlantaDto & { idPlanta: number })) => Promise; + initialData?: PlantaDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const PlantaFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [nombre, setNombre] = useState(''); + const [detalle, setDetalle] = useState(''); // Detalle es string, no opcional según la BD + const [loading, setLoading] = useState(false); + const [localErrorNombre, setLocalErrorNombre] = useState(null); + const [localErrorDetalle, setLocalErrorDetalle] = useState(null); + + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + setNombre(initialData?.nombre || ''); + setDetalle(initialData?.detalle || ''); // Inicializar detalle + setLocalErrorNombre(null); + setLocalErrorDetalle(null); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const handleInputChange = (field: 'nombre' | 'detalle') => { + if (field === 'nombre' && localErrorNombre) setLocalErrorNombre(null); + if (field === 'detalle' && localErrorDetalle) setLocalErrorDetalle(null); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setLocalErrorNombre(null); + setLocalErrorDetalle(null); + clearErrorMessage(); + + let hasError = false; + if (!nombre.trim()) { + setLocalErrorNombre('El nombre es obligatorio.'); + hasError = true; + } + if (!detalle.trim()) { // Detalle también es requerido según la BD + setLocalErrorDetalle('El detalle es obligatorio.'); + hasError = true; + } + + if (hasError) return; + + setLoading(true); + try { + const dataToSubmit = { nombre, detalle }; // Detalle siempre se envía + + if (isEditing && initialData) { + await onSubmit({ ...dataToSubmit, idPlanta: initialData.idPlanta }); + } else { + await onSubmit(dataToSubmit as CreatePlantaDto); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de PlantaFormModal:", error); + // El error API se muestra vía 'errorMessage' + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Planta de Impresión' : 'Agregar Nueva Planta de Impresión'} + + + { setNombre(e.target.value); handleInputChange('nombre'); }} + margin="normal" + error={!!localErrorNombre} + helperText={localErrorNombre || ''} + disabled={loading} + autoFocus + /> + { setDetalle(e.target.value); handleInputChange('detalle'); }} + margin="normal" + multiline + rows={3} + error={!!localErrorDetalle} // Añadir manejo de error para detalle + helperText={localErrorDetalle || ''} // Mostrar error de detalle + disabled={loading} + /> + {errorMessage && {errorMessage}} + + + + + + + + + ); +}; + +export default PlantaFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/TipoBobinaFormModal.tsx b/Frontend/src/components/Modals/TipoBobinaFormModal.tsx new file mode 100644 index 0000000..6798867 --- /dev/null +++ b/Frontend/src/components/Modals/TipoBobinaFormModal.tsx @@ -0,0 +1,117 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; +import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; +import type { CreateTipoBobinaDto } from '../../models/dtos/Impresion/CreateTipoBobinaDto'; +import type { UpdateTipoBobinaDto } from '../../models/dtos/Impresion/UpdateTipoBobinaDto'; + +const modalStyle = { /* ... (mismo estilo que otros modales) ... */ + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + +interface TipoBobinaFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateTipoBobinaDto | (UpdateTipoBobinaDto & { idTipoBobina: number })) => Promise; + initialData?: TipoBobinaDto | null; + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const TipoBobinaFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [denominacion, setDenominacion] = useState(''); + const [loading, setLoading] = useState(false); + const [localError, setLocalError] = useState(null); + + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + setDenominacion(initialData?.denominacion || ''); + setLocalError(null); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const handleInputChange = () => { + if (localError) setLocalError(null); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setLocalError(null); + clearErrorMessage(); + + if (!denominacion.trim()) { + setLocalError('La denominación es obligatoria.'); + return; + } + + setLoading(true); + try { + const dataToSubmit = { denominacion }; + + if (isEditing && initialData) { + await onSubmit({ ...dataToSubmit, idTipoBobina: initialData.idTipoBobina }); + } else { + await onSubmit(dataToSubmit as CreateTipoBobinaDto); + } + onClose(); + } catch (error: any) { + console.error("Error en submit de TipoBobinaFormModal:", error); + // El error API se muestra vía 'errorMessage' + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Tipo de Bobina' : 'Agregar Nuevo Tipo de Bobina'} + + + { setDenominacion(e.target.value); handleInputChange(); }} + margin="normal" + error={!!localError} + helperText={localError || ''} + disabled={loading} + autoFocus + /> + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default TipoBobinaFormModal; \ No newline at end of file diff --git a/Frontend/src/components/Modals/ZonaFormModal.tsx b/Frontend/src/components/Modals/ZonaFormModal.tsx new file mode 100644 index 0000000..aa85342 --- /dev/null +++ b/Frontend/src/components/Modals/ZonaFormModal.tsx @@ -0,0 +1,128 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material'; +import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // Usamos el DTO de la API para listar +import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; + +const modalStyle = { + position: 'absolute' as 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 4, +}; + + +interface ZonaFormModalProps { + open: boolean; + onClose: () => void; + onSubmit: (data: CreateZonaDto | (CreateZonaDto & { idZona: number })) => Promise; + initialData?: ZonaDto | null; // Puede recibir una Zona para editar + errorMessage?: string | null; + clearErrorMessage: () => void; +} + +const ZonaFormModal: React.FC = ({ + open, + onClose, + onSubmit, + initialData, + errorMessage, + clearErrorMessage +}) => { + const [nombre, setNombre] = useState(''); + const [descripcion, setDescripcion] = useState(''); + const [loading, setLoading] = useState(false); + const [localError, setLocalError] = useState(null); + + const isEditing = Boolean(initialData); + + useEffect(() => { + if (open) { + setNombre(initialData?.nombre || ''); + setDescripcion(initialData?.descripcion || ''); + setLocalError(null); + clearErrorMessage(); + } + }, [open, initialData, clearErrorMessage]); + + const handleInputChange = () => { + if (localError) setLocalError(null); + if (errorMessage) clearErrorMessage(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setLocalError(null); + clearErrorMessage(); + + if (!nombre.trim()) { + setLocalError('El nombre es obligatorio.'); + return; + } + + setLoading(true); + try { + const dataToSubmit: CreateZonaDto = { nombre, descripcion: descripcion || undefined }; + if (isEditing && initialData) { + await onSubmit({ ...dataToSubmit, idZona: initialData.idZona }); // Añadir idZona si es edición + } else { + await onSubmit(dataToSubmit); + } + onClose(); // Cerrar en éxito + } catch (error: any) { + console.error("Error en submit de ZonaFormModal:", error); + // El error API se muestra a través de errorMessage + } finally { + setLoading(false); + } + }; + + return ( + + + + {isEditing ? 'Editar Zona' : 'Agregar Nueva Zona'} + + + { setNombre(e.target.value); handleInputChange(); }} + margin="normal" + error={!!localError && !nombre.trim()} + helperText={localError && !nombre.trim() ? localError : ''} + disabled={loading} + autoFocus + /> + { setDescripcion(e.target.value); handleInputChange();}} + margin="normal" + multiline + rows={3} + disabled={loading} + /> + {errorMessage && {errorMessage}} + + + + + + + + ); +}; + +export default ZonaFormModal; \ No newline at end of file diff --git a/Frontend/src/models/Empresa.ts b/Frontend/src/models/Empresa.ts new file mode 100644 index 0000000..6bb52e1 --- /dev/null +++ b/Frontend/src/models/Empresa.ts @@ -0,0 +1,5 @@ +export interface Empresa { + idEmpresa: number; + nombre: string; + detalle?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Empresas/CreateEmpresaDto.ts b/Frontend/src/models/dtos/Empresas/CreateEmpresaDto.ts new file mode 100644 index 0000000..73135e0 --- /dev/null +++ b/Frontend/src/models/dtos/Empresas/CreateEmpresaDto.ts @@ -0,0 +1,4 @@ +export interface CreateEmpresaDto { + nombre: string; + detalle?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Empresas/EmpresaDto.ts b/Frontend/src/models/dtos/Empresas/EmpresaDto.ts new file mode 100644 index 0000000..416eebf --- /dev/null +++ b/Frontend/src/models/dtos/Empresas/EmpresaDto.ts @@ -0,0 +1,5 @@ +export interface EmpresaDto { + idEmpresa: number; + nombre: string; + detalle?: string; // Puede ser null o undefined +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Empresas/UpdateEmpresaDto.ts b/Frontend/src/models/dtos/Empresas/UpdateEmpresaDto.ts new file mode 100644 index 0000000..a9868ef --- /dev/null +++ b/Frontend/src/models/dtos/Empresas/UpdateEmpresaDto.ts @@ -0,0 +1,4 @@ +export interface UpdateEmpresaDto { + nombre: string; + detalle?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/CreateEstadoBobinaDto.ts b/Frontend/src/models/dtos/Impresion/CreateEstadoBobinaDto.ts new file mode 100644 index 0000000..871ff75 --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/CreateEstadoBobinaDto.ts @@ -0,0 +1,4 @@ +export interface CreateEstadoBobinaDto { + denominacion: string; + obs?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/CreatePlantaDto.ts b/Frontend/src/models/dtos/Impresion/CreatePlantaDto.ts new file mode 100644 index 0000000..192aa4a --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/CreatePlantaDto.ts @@ -0,0 +1,4 @@ +export interface CreatePlantaDto { + nombre: string; + detalle: string; // Basado en la tabla, no es opcional +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/CreateTipoBobinaDto.ts b/Frontend/src/models/dtos/Impresion/CreateTipoBobinaDto.ts new file mode 100644 index 0000000..c64cbce --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/CreateTipoBobinaDto.ts @@ -0,0 +1,3 @@ +export interface CreateTipoBobinaDto { + denominacion: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/EstadoBobinaDto.ts b/Frontend/src/models/dtos/Impresion/EstadoBobinaDto.ts new file mode 100644 index 0000000..6414255 --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/EstadoBobinaDto.ts @@ -0,0 +1,5 @@ +export interface EstadoBobinaDto { + idEstadoBobina: number; + denominacion: string; + obs?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/PlantaDto.ts b/Frontend/src/models/dtos/Impresion/PlantaDto.ts new file mode 100644 index 0000000..e07c70a --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/PlantaDto.ts @@ -0,0 +1,5 @@ +export interface PlantaDto { + idPlanta: number; + nombre: string; + detalle: string; // Basado en la tabla, no es opcional +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/TipoBobinaDto.ts b/Frontend/src/models/dtos/Impresion/TipoBobinaDto.ts new file mode 100644 index 0000000..f5d2bcb --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/TipoBobinaDto.ts @@ -0,0 +1,4 @@ +export interface TipoBobinaDto { + idTipoBobina: number; + denominacion: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/UpdateEstadoBobinaDto.ts b/Frontend/src/models/dtos/Impresion/UpdateEstadoBobinaDto.ts new file mode 100644 index 0000000..fc864bb --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/UpdateEstadoBobinaDto.ts @@ -0,0 +1,4 @@ +export interface UpdateEstadoBobinaDto { + denominacion: string; + obs?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/UpdatePlantaDto.ts b/Frontend/src/models/dtos/Impresion/UpdatePlantaDto.ts new file mode 100644 index 0000000..afb2792 --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/UpdatePlantaDto.ts @@ -0,0 +1,4 @@ +export interface UpdatePlantaDto { + nombre: string; + detalle: string; // Basado en la tabla, no es opcional +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Impresion/UpdateTipoBobinaDto.ts b/Frontend/src/models/dtos/Impresion/UpdateTipoBobinaDto.ts new file mode 100644 index 0000000..4eddda1 --- /dev/null +++ b/Frontend/src/models/dtos/Impresion/UpdateTipoBobinaDto.ts @@ -0,0 +1,3 @@ +export interface UpdateTipoBobinaDto { + denominacion: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Zonas/CreateZonaDto.ts b/Frontend/src/models/dtos/Zonas/CreateZonaDto.ts new file mode 100644 index 0000000..488f38d --- /dev/null +++ b/Frontend/src/models/dtos/Zonas/CreateZonaDto.ts @@ -0,0 +1,4 @@ +export interface CreateZonaDto { + nombre: string; + descripcion?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Zonas/UpdateZonaDto.ts b/Frontend/src/models/dtos/Zonas/UpdateZonaDto.ts new file mode 100644 index 0000000..85d08c6 --- /dev/null +++ b/Frontend/src/models/dtos/Zonas/UpdateZonaDto.ts @@ -0,0 +1,4 @@ +export interface UpdateZonaDto { + nombre: string; + descripcion?: string; +} \ No newline at end of file diff --git a/Frontend/src/models/dtos/Zonas/ZonaDto.ts b/Frontend/src/models/dtos/Zonas/ZonaDto.ts new file mode 100644 index 0000000..abb0677 --- /dev/null +++ b/Frontend/src/models/dtos/Zonas/ZonaDto.ts @@ -0,0 +1,6 @@ +export interface ZonaDto { + idZona: number; + nombre: string; + descripcion?: string; + // Nota: No incluye 'estado', ya que la API por defecto devuelve solo activas +} \ No newline at end of file diff --git a/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx b/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx index 8ff8417..ddaa0d5 100644 --- a/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx +++ b/Frontend/src/pages/Contables/GestionarTiposPagoPage.tsx @@ -148,14 +148,16 @@ const GestionarTiposPagoPage: React.FC = () => { {/* */} {puedeCrear && ( - + + + )} diff --git a/Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx b/Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx index 3f77347..531295e 100644 --- a/Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx +++ b/Frontend/src/pages/Distribucion/DistribucionIndexPage.tsx @@ -1,7 +1,7 @@ // src/pages/distribucion/DistribucionIndexPage.tsx import React, { useState, useEffect } from 'react'; import { Box, Tabs, Tab, Paper, Typography } from '@mui/material'; -import { Outlet, useNavigate, useLocation, Link as RouterLink } from 'react-router-dom'; +import { Outlet, useNavigate, useLocation } from 'react-router-dom'; // Define las sub-pestañas del módulo Distribución // El path es relativo a la ruta base del módulo (ej: /distribucion) diff --git a/Frontend/src/pages/Distribucion/EmpresasPage.tsx b/Frontend/src/pages/Distribucion/EmpresasPage.tsx deleted file mode 100644 index ebed34c..0000000 --- a/Frontend/src/pages/Distribucion/EmpresasPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; - -const EmpresasPage: React.FC = () => { - return Página de Gestión de Empresas; -}; -export default EmpresasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarEmpresasPage.tsx b/Frontend/src/pages/Distribucion/GestionarEmpresasPage.tsx new file mode 100644 index 0000000..3457670 --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarEmpresasPage.tsx @@ -0,0 +1,276 @@ +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 empresaService from '../../services/empresaService'; // Importar el servicio de Empresas +import type { EmpresaDto } from '../../models/dtos/Empresas/EmpresaDto'; +import type { CreateEmpresaDto } from '../../models/dtos/Empresas/CreateEmpresaDto'; +import type { UpdateEmpresaDto } from '../../models/dtos/Empresas/UpdateEmpresaDto'; +import EmpresaFormModal from '../../components/Modals/EmpresaFormModal'; // Importar el modal de Empresas +import { usePermissions } from '../../hooks/usePermissions'; // Importar hook de permisos +import axios from 'axios'; // Para manejo de errores de API + +const GestionarEmpresasPage: React.FC = () => { + const [empresas, setEmpresas] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); // Para errores al cargar datos + const [filtroNombre, setFiltroNombre] = useState(''); + // const [filtroDetalle, setFiltroDetalle] = useState(''); // Descomentar si añades filtro por detalle + + const [modalOpen, setModalOpen] = useState(false); + const [editingEmpresa, setEditingEmpresa] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); // Para errores del modal (Create/Update/Delete) + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + // Para el menú contextual de acciones por fila + const [anchorEl, setAnchorEl] = useState(null); + const [selectedEmpresaRow, setSelectedEmpresaRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); // Usar el hook + + // Determinar permisos específicos para Empresas (basado en los códigos DE001 a DE004) + const puedeVer = isSuperAdmin || tienePermiso("DE001"); // Necesario para mostrar la página + const puedeCrear = isSuperAdmin || tienePermiso("DE002"); + const puedeModificar = isSuperAdmin || tienePermiso("DE003"); + const puedeEliminar = isSuperAdmin || tienePermiso("DE004"); + + const cargarEmpresas = useCallback(async () => { + if (!puedeVer) { // Si no tiene permiso de ver, no cargar nada + setError("No tiene permiso para ver esta sección."); + setLoading(false); + return; + } + setLoading(true); + setError(null); + setApiErrorMessage(null); // Limpiar errores API al recargar + try { + const data = await empresaService.getAllEmpresas(filtroNombre/*, filtroDetalle*/); + setEmpresas(data); + } catch (err) { + console.error(err); + setError('Error al cargar las empresas.'); + } finally { + setLoading(false); + } + }, [filtroNombre, puedeVer /*, filtroDetalle*/]); // Añadir puedeVer como dependencia + + useEffect(() => { + cargarEmpresas(); + }, [cargarEmpresas]); // Ejecutar al montar y cuando cambien las dependencias de cargarEmpresas + + const handleOpenModal = (empresa?: EmpresaDto) => { + setEditingEmpresa(empresa || null); + setApiErrorMessage(null); // Limpiar error API antes de abrir modal + setModalOpen(true); + }; + + const handleCloseModal = () => { + setModalOpen(false); + setEditingEmpresa(null); + // No limpiamos apiErrorMessage aquí, se limpia al intentar un nuevo submit o al recargar + }; + + const handleSubmitModal = async (data: CreateEmpresaDto | (UpdateEmpresaDto & { idEmpresa: number })) => { + setApiErrorMessage(null); // Limpiar error previo + // No necesitamos setLoading aquí, el modal lo maneja + try { + if (editingEmpresa && 'idEmpresa' in data) { // Es Update (verificamos si initialData existe Y data tiene id) + await empresaService.updateEmpresa(editingEmpresa.idEmpresa, data); + } else { // Es Create + await empresaService.createEmpresa(data as CreateEmpresaDto); + } + cargarEmpresas(); // Recargar lista en éxito + // handleCloseModal(); // El modal se cierra solo desde su propio onSubmit exitoso + } catch (err: any) { + console.error("Error en submit modal (padre):", err); + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Ocurrió un error inesperado al guardar la empresa.'; + setApiErrorMessage(message); + throw err; // Re-lanzar para que el modal sepa que hubo error y no se cierre + } + // No poner finally setLoading(false) aquí, el modal lo controla + }; + + const handleDelete = async (id: number) => { + // Opcional: mostrar un mensaje de confirmación más detallado + if (window.confirm(`¿Está seguro de que desea eliminar esta empresa (ID: ${id})? Esta acción también eliminará los saldos asociados.`)) { + setApiErrorMessage(null); // Limpiar errores previos + try { + await empresaService.deleteEmpresa(id); + cargarEmpresas(); // Recargar la lista para reflejar la eliminación + } catch (err: any) { + console.error("Error al eliminar empresa:", err); + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Ocurrió un error inesperado al eliminar la empresa.'; + setApiErrorMessage(message); // Mostrar error de API + } + } + handleMenuClose(); // Cerrar el menú de acciones + }; + + // --- Handlers para el menú y paginación (sin cambios respecto a Zonas/TiposPago) --- + const handleMenuOpen = (event: React.MouseEvent, empresa: EmpresaDto) => { + setAnchorEl(event.currentTarget); + setSelectedEmpresaRow(empresa); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + setSelectedEmpresaRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + // Datos a mostrar en la tabla actual según paginación + const displayData = empresas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + // Si no tiene permiso para ver, mostrar mensaje y salir + if (!loading && !puedeVer) { + return ( + + Gestionar Empresas + {error || "No tiene permiso para acceder a esta sección."} + + ); + } + + return ( + + + Gestionar Empresas + + + + + setFiltroNombre(e.target.value)} + // Puedes añadir un botón de buscar explícito o dejar que filtre al escribir + /> + + {/* Mostrar botón de agregar solo si tiene permiso */} + {puedeCrear && ( + + + + )} + + + {/* Indicador de carga */} + {loading && } + {/* Mensaje de error al cargar datos */} + {error && !loading && {error}} + {/* Mensaje de error de la API (modal/delete) */} + {apiErrorMessage && {apiErrorMessage}} + + {/* Tabla de datos (solo si no está cargando y no hubo error de carga inicial) */} + {!loading && !error && ( + + + + + Nombre + Detalle + {/* Mostrar columna de acciones solo si tiene permiso de modificar o eliminar */} + {(puedeModificar || puedeEliminar) && Acciones} + + + + {displayData.length === 0 && !loading ? ( + No se encontraron empresas. + ) : ( + displayData.map((emp) => ( + + {emp.nombre} + {emp.detalle || '-'} + {/* Mostrar botón de acciones solo si tiene permiso */} + {(puedeModificar || puedeEliminar) && ( + + handleMenuOpen(e, emp)} + // Deshabilitar si no tiene ningún permiso específico (redundante por la condición de la celda, pero seguro) + disabled={!puedeModificar && !puedeEliminar} + > + + + + )} + + )) + )} + +
+ {/* Paginación */} + +
+ )} + + {/* Menú contextual para acciones de fila */} + + {/* Mostrar opción Modificar solo si tiene permiso */} + {puedeModificar && ( + { handleOpenModal(selectedEmpresaRow!); handleMenuClose(); }}> + Modificar + + )} + {/* Mostrar opción Eliminar solo si tiene permiso */} + {puedeEliminar && ( + handleDelete(selectedEmpresaRow!.idEmpresa)}> + Eliminar + + )} + {/* Mensaje si no hay acciones disponibles (por si acaso) */} + {(!puedeModificar && !puedeEliminar) && Sin acciones} + + + {/* Modal para Crear/Editar */} + setApiErrorMessage(null)} // Pasar función para limpiar el error + /> +
+ ); +}; + +export default GestionarEmpresasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/GestionarZonasPage.tsx b/Frontend/src/pages/Distribucion/GestionarZonasPage.tsx new file mode 100644 index 0000000..1e03b4b --- /dev/null +++ b/Frontend/src/pages/Distribucion/GestionarZonasPage.tsx @@ -0,0 +1,244 @@ +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 zonaService from '../../services/zonaService'; // Servicio de Zonas +import type { ZonaDto } from '../../models/dtos/Zonas/ZonaDto'; // DTO de Zonas +import type { CreateZonaDto } from '../../models/dtos/Zonas/CreateZonaDto'; // DTOs Create +import type { UpdateZonaDto } from '../../models/dtos/Zonas/UpdateZonaDto'; // DTOs Update +import ZonaFormModal from '../../components/Modals/ZonaFormModal'; // Modal de Zonas +import { usePermissions } from '../../hooks/usePermissions'; // Hook de permisos +import axios from 'axios'; // Para manejo de errores + +const GestionarZonasPage: React.FC = () => { + const [zonas, setZonas] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroNombre, setFiltroNombre] = useState(''); + // const [filtroDescripcion, setFiltroDescripcion] = useState(''); // Si añades filtro por descripción + + const [modalOpen, setModalOpen] = useState(false); + const [editingZona, setEditingZona] = useState(null); // Usar ZonaDto aquí también + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedZonaRow, setSelectedZonaRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + + // Ajustar códigos de permiso para Zonas + const puedeCrear = isSuperAdmin || tienePermiso("ZD002"); + const puedeModificar = isSuperAdmin || tienePermiso("ZD003"); + const puedeEliminar = isSuperAdmin || tienePermiso("ZD004"); + + + const cargarZonas = useCallback(async () => { + setLoading(true); + setError(null); + try { + // Usar servicio de zonas y filtros + const data = await zonaService.getAllZonas(filtroNombre/*, filtroDescripcion*/); + setZonas(data); + } catch (err) { + console.error(err); + setError('Error al cargar las zonas.'); + } finally { + setLoading(false); + } + }, [filtroNombre/*, filtroDescripcion*/]); // Añadir dependencias de filtro + + useEffect(() => { + cargarZonas(); + }, [cargarZonas]); + + const handleOpenModal = (zona?: ZonaDto) => { + setEditingZona(zona || null); + setApiErrorMessage(null); + setModalOpen(true); + }; + + const handleCloseModal = () => { + setModalOpen(false); + setEditingZona(null); + }; + + const handleSubmitModal = async (data: CreateZonaDto | UpdateZonaDto) => { + setApiErrorMessage(null); + try { + if (editingZona) { // Es Update + await zonaService.updateZona(editingZona.idZona, data as UpdateZonaDto); + } else { // Es Create + await zonaService.createZona(data as CreateZonaDto); + } + cargarZonas(); // Recargar lista + } catch (err: any) { + console.error("Error en submit modal (padre):", err); + if (axios.isAxiosError(err) && err.response) { + setApiErrorMessage(err.response.data?.message || 'Error al guardar la zona.'); + } else { + setApiErrorMessage('Ocurrió un error inesperado al guardar la zona.'); + } + throw err; + } + }; + + + const handleDelete = async (id: number) => { + if (window.confirm('¿Está seguro de que desea eliminar esta zona? (Se marcará como inactiva)')) { + setApiErrorMessage(null); + try { + await zonaService.deleteZona(id); // Llama al soft delete + cargarZonas(); // Recarga la lista (la zona eliminada ya no debería aparecer si el filtro es solo activas) + } catch (err: any) { + console.error("Error al eliminar zona:", err); + if (axios.isAxiosError(err) && err.response) { + setApiErrorMessage(err.response.data?.message || 'Error al eliminar.'); + } else { + setApiErrorMessage('Ocurrió un error inesperado al eliminar.'); + } + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, zona: ZonaDto) => { + setAnchorEl(event.currentTarget); + setSelectedZonaRow(zona); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + setSelectedZonaRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + // Adaptar para paginación + const displayData = zonas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + + return ( + + + Gestionar Zonas + + + + + setFiltroNombre(e.target.value)} + /> + {/* */} + + {puedeCrear && ( + + + + )} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + + {!loading && !error && ( + + + + + Nombre + Descripción + Acciones + + + + {displayData.length === 0 && !loading ? ( + No se encontraron zonas. + ) : ( + displayData.map((zona) => ( + + {zona.nombre} + {zona.descripcion || '-'} + + handleMenuOpen(e, zona)} + disabled={!puedeModificar && !puedeEliminar} + > + + + + + )) + )} + +
+ +
+ )} + + + {puedeModificar && ( + { handleOpenModal(selectedZonaRow!); handleMenuClose(); }}> + Modificar + + )} + {puedeEliminar && ( + handleDelete(selectedZonaRow!.idZona)}> + Eliminar + + )} + {(!puedeModificar && !puedeEliminar) && Sin acciones} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarZonasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Distribucion/ZonasPage.tsx b/Frontend/src/pages/Distribucion/ZonasPage.tsx deleted file mode 100644 index 4e64883..0000000 --- a/Frontend/src/pages/Distribucion/ZonasPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; - -const ZonasPage: React.FC = () => { - return Página de Gestión de Zonas; -}; -export default ZonasPage; \ No newline at end of file diff --git a/Frontend/src/pages/HomePage.tsx b/Frontend/src/pages/HomePage.tsx index c7f957f..0d12f80 100644 --- a/Frontend/src/pages/HomePage.tsx +++ b/Frontend/src/pages/HomePage.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { Typography, Container } from '@mui/material'; +import { Typography, Container, Box } from '@mui/material'; +import logo from '../assets/eldia.png'; const HomePage: React.FC = () => { return ( @@ -10,6 +11,21 @@ const HomePage: React.FC = () => { Seleccione una opción del menú principal para comenzar. + + Logo + ); }; diff --git a/Frontend/src/pages/Impresion/GestionarEstadosBobinaPage.tsx b/Frontend/src/pages/Impresion/GestionarEstadosBobinaPage.tsx new file mode 100644 index 0000000..e7cabd9 --- /dev/null +++ b/Frontend/src/pages/Impresion/GestionarEstadosBobinaPage.tsx @@ -0,0 +1,252 @@ +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 estadoBobinaService from '../../services/estadoBobinaService'; +import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; +import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto'; +import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto'; +import EstadoBobinaFormModal from '../../components/Modals/EstadoBobinaFormModal'; +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarEstadosBobinaPage: React.FC = () => { + const [estadosBobina, setEstadosBobina] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroDenominacion, setFiltroDenominacion] = useState(''); + + const [modalOpen, setModalOpen] = useState(false); + const [editingEstado, setEditingEstado] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedEstadoRow, setSelectedEstadoRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + + // Permisos para Estados de Bobina (ej: IB010 a IB013) + const puedeVer = isSuperAdmin || tienePermiso("IB010"); + const puedeCrear = isSuperAdmin || tienePermiso("IB011"); + const puedeModificar = isSuperAdmin || tienePermiso("IB012"); + const puedeEliminar = isSuperAdmin || tienePermiso("IB013"); + + const cargarEstadosBobina = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); + setLoading(false); + return; + } + setLoading(true); + setError(null); + setApiErrorMessage(null); + try { + const data = await estadoBobinaService.getAllEstadosBobina(filtroDenominacion); + setEstadosBobina(data); + } catch (err) { + console.error(err); + setError('Error al cargar los estados de bobina.'); + } finally { + setLoading(false); + } + }, [filtroDenominacion, puedeVer]); + + useEffect(() => { + cargarEstadosBobina(); + }, [cargarEstadosBobina]); + + const handleOpenModal = (estado?: EstadoBobinaDto) => { + setEditingEstado(estado || null); + setApiErrorMessage(null); + setModalOpen(true); + }; + + const handleCloseModal = () => { + setModalOpen(false); + setEditingEstado(null); + }; + + const handleSubmitModal = async (data: CreateEstadoBobinaDto | (UpdateEstadoBobinaDto & { idEstadoBobina: number })) => { + setApiErrorMessage(null); + try { + if (editingEstado && 'idEstadoBobina' in data) { + await estadoBobinaService.updateEstadoBobina(editingEstado.idEstadoBobina, data); + } else { + await estadoBobinaService.createEstadoBobina(data as CreateEstadoBobinaDto); + } + cargarEstadosBobina(); + } catch (err: any) { + console.error("Error en submit modal (padre - EstadosBobina):", err); + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Ocurrió un error inesperado al guardar el estado de bobina.'; + setApiErrorMessage(message); + throw err; + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm(`¿Está seguro de que desea eliminar este estado de bobina (ID: ${id})?`)) { + setApiErrorMessage(null); + try { + await estadoBobinaService.deleteEstadoBobina(id); + cargarEstadosBobina(); + } catch (err: any) { + console.error("Error al eliminar estado de bobina:", err); + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Ocurrió un error inesperado al eliminar el estado de bobina.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, estado: EstadoBobinaDto) => { + setAnchorEl(event.currentTarget); + setSelectedEstadoRow(estado); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + setSelectedEstadoRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const displayData = estadosBobina.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeVer) { + return ( + + Gestionar Estados de Bobina + {error || "No tiene permiso para acceder a esta sección."} + + ); + } + + return ( + + + Gestionar Estados de Bobina + + + + + setFiltroDenominacion(e.target.value)} + /> + + {puedeCrear && ( + + + + )} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && ( + + + + + Denominación + Observación + {(puedeModificar || puedeEliminar) && Acciones} + + + + {displayData.length === 0 && !loading ? ( + No se encontraron estados de bobina. + ) : ( + displayData.map((estado) => ( + + {estado.denominacion} + {estado.obs || '-'} + {(puedeModificar || puedeEliminar) && ( + + handleMenuOpen(e, estado)} + disabled={!puedeModificar && !puedeEliminar} + > + + + + )} + + )) + )} + +
+ +
+ )} + + + {puedeModificar && ( + { handleOpenModal(selectedEstadoRow!); handleMenuClose(); }}> + Modificar + + )} + {puedeEliminar && ( + handleDelete(selectedEstadoRow!.idEstadoBobina)}> + Eliminar + + )} + {(!puedeModificar && !puedeEliminar) && Sin acciones} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarEstadosBobinaPage; \ No newline at end of file diff --git a/Frontend/src/pages/Impresion/GestionarPlantasPage.tsx b/Frontend/src/pages/Impresion/GestionarPlantasPage.tsx new file mode 100644 index 0000000..6305286 --- /dev/null +++ b/Frontend/src/pages/Impresion/GestionarPlantasPage.tsx @@ -0,0 +1,263 @@ +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 plantaService from '../../services/plantaService'; // Servicio de Plantas +import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; +import type { CreatePlantaDto } from '../../models/dtos/Impresion/CreatePlantaDto'; +import type { UpdatePlantaDto } from '../../models/dtos/Impresion/UpdatePlantaDto'; +import PlantaFormModal from '../../components/Modals/PlantaFormModal'; // Modal de Plantas +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarPlantasPage: React.FC = () => { + const [plantas, setPlantas] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroNombre, setFiltroNombre] = useState(''); + const [filtroDetalle, setFiltroDetalle] = useState(''); // Añadir filtro por detalle si se desea + + const [modalOpen, setModalOpen] = useState(false); + const [editingPlanta, setEditingPlanta] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedPlantaRow, setSelectedPlantaRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + + // Permisos específicos para Plantas (IP001 a IP004) + const puedeVer = isSuperAdmin || tienePermiso("IP001"); + const puedeCrear = isSuperAdmin || tienePermiso("IP002"); + const puedeModificar = isSuperAdmin || tienePermiso("IP003"); + const puedeEliminar = isSuperAdmin || tienePermiso("IP004"); + + const cargarPlantas = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); + setLoading(false); + return; + } + setLoading(true); + setError(null); + setApiErrorMessage(null); + try { + // Pasar ambos filtros al servicio + const data = await plantaService.getAllPlantas(filtroNombre, filtroDetalle); + setPlantas(data); + } catch (err) { + console.error(err); + setError('Error al cargar las plantas de impresión.'); + } finally { + setLoading(false); + } + // Añadir filtroDetalle a las dependencias si se usa + }, [filtroNombre, filtroDetalle, puedeVer]); + + useEffect(() => { + cargarPlantas(); + }, [cargarPlantas]); + + const handleOpenModal = (planta?: PlantaDto) => { + setEditingPlanta(planta || null); + setApiErrorMessage(null); + setModalOpen(true); + }; + + const handleCloseModal = () => { + setModalOpen(false); + setEditingPlanta(null); + }; + + const handleSubmitModal = async (data: CreatePlantaDto | (UpdatePlantaDto & { idPlanta: number })) => { + setApiErrorMessage(null); + try { + if (editingPlanta && 'idPlanta' in data) { + await plantaService.updatePlanta(editingPlanta.idPlanta, data); + } else { + await plantaService.createPlanta(data as CreatePlantaDto); + } + cargarPlantas(); + } catch (err: any) { + console.error("Error en submit modal (padre - Plantas):", err); + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Ocurrió un error inesperado al guardar la planta.'; + setApiErrorMessage(message); + throw err; + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm(`¿Está seguro de que desea eliminar esta planta (ID: ${id})?`)) { + setApiErrorMessage(null); + try { + await plantaService.deletePlanta(id); + cargarPlantas(); + } catch (err: any) { + console.error("Error al eliminar planta:", err); + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Ocurrió un error inesperado al eliminar la planta.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, planta: PlantaDto) => { + setAnchorEl(event.currentTarget); + setSelectedPlantaRow(planta); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + setSelectedPlantaRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const displayData = plantas.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeVer) { + return ( + + Gestionar Plantas de Impresión + {error || "No tiene permiso para acceder a esta sección."} + + ); + } + + return ( + + + Gestionar Plantas de Impresión + + + + + setFiltroNombre(e.target.value)} + /> + setFiltroDetalle(e.target.value)} + /> + {/* */} + + {puedeCrear && ( + + + + )} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && ( + + + + + Nombre + Detalle + {(puedeModificar || puedeEliminar) && Acciones} + + + + {displayData.length === 0 && !loading ? ( + No se encontraron plantas de impresión. + ) : ( + displayData.map((planta) => ( + + {planta.nombre} + {planta.detalle} {/* Detalle no es opcional */} + {(puedeModificar || puedeEliminar) && ( + + handleMenuOpen(e, planta)} + disabled={!puedeModificar && !puedeEliminar} + > + + + + )} + + )) + )} + +
+ +
+ )} + + + {puedeModificar && ( + { handleOpenModal(selectedPlantaRow!); handleMenuClose(); }}> + Modificar + + )} + {puedeEliminar && ( + handleDelete(selectedPlantaRow!.idPlanta)}> + Eliminar + + )} + {(!puedeModificar && !puedeEliminar) && Sin acciones} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarPlantasPage; \ No newline at end of file diff --git a/Frontend/src/pages/Impresion/GestionarTiposBobinaPage.tsx b/Frontend/src/pages/Impresion/GestionarTiposBobinaPage.tsx new file mode 100644 index 0000000..e7a9be6 --- /dev/null +++ b/Frontend/src/pages/Impresion/GestionarTiposBobinaPage.tsx @@ -0,0 +1,251 @@ +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 tipoBobinaService from '../../services/tipoBobinaService'; // Servicio específico +import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; +import type { CreateTipoBobinaDto } from '../../models/dtos/Impresion/CreateTipoBobinaDto'; +import type { UpdateTipoBobinaDto } from '../../models/dtos/Impresion/UpdateTipoBobinaDto'; +import TipoBobinaFormModal from '../../components/Modals/TipoBobinaFormModal'; // Modal específico +import { usePermissions } from '../../hooks/usePermissions'; +import axios from 'axios'; + +const GestionarTiposBobinaPage: React.FC = () => { + const [tiposBobina, setTiposBobina] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filtroDenominacion, setFiltroDenominacion] = useState(''); + + const [modalOpen, setModalOpen] = useState(false); + const [editingTipoBobina, setEditingTipoBobina] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedTipoBobinaRow, setSelectedTipoBobinaRow] = useState(null); + + const { tienePermiso, isSuperAdmin } = usePermissions(); + + // Permisos específicos para Tipos de Bobina (IB006 a IB009) + const puedeVer = isSuperAdmin || tienePermiso("IB006"); + const puedeCrear = isSuperAdmin || tienePermiso("IB007"); + const puedeModificar = isSuperAdmin || tienePermiso("IB008"); + const puedeEliminar = isSuperAdmin || tienePermiso("IB009"); + + const cargarTiposBobina = useCallback(async () => { + if (!puedeVer) { + setError("No tiene permiso para ver esta sección."); + setLoading(false); + return; + } + setLoading(true); + setError(null); + setApiErrorMessage(null); + try { + const data = await tipoBobinaService.getAllTiposBobina(filtroDenominacion); + setTiposBobina(data); + } catch (err) { + console.error(err); + setError('Error al cargar los tipos de bobina.'); + } finally { + setLoading(false); + } + }, [filtroDenominacion, puedeVer]); + + useEffect(() => { + cargarTiposBobina(); + }, [cargarTiposBobina]); + + const handleOpenModal = (tipoBobina?: TipoBobinaDto) => { + setEditingTipoBobina(tipoBobina || null); + setApiErrorMessage(null); + setModalOpen(true); + }; + + const handleCloseModal = () => { + setModalOpen(false); + setEditingTipoBobina(null); + }; + + const handleSubmitModal = async (data: CreateTipoBobinaDto | (UpdateTipoBobinaDto & { idTipoBobina: number })) => { + setApiErrorMessage(null); + try { + if (editingTipoBobina && 'idTipoBobina' in data) { + await tipoBobinaService.updateTipoBobina(editingTipoBobina.idTipoBobina, data); + } else { + await tipoBobinaService.createTipoBobina(data as CreateTipoBobinaDto); + } + cargarTiposBobina(); + } catch (err: any) { + console.error("Error en submit modal (padre - TiposBobina):", err); + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Ocurrió un error inesperado al guardar el tipo de bobina.'; + setApiErrorMessage(message); + throw err; + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm(`¿Está seguro de que desea eliminar este tipo de bobina (ID: ${id})?`)) { + setApiErrorMessage(null); + try { + await tipoBobinaService.deleteTipoBobina(id); + cargarTiposBobina(); + } catch (err: any) { + console.error("Error al eliminar tipo de bobina:", err); + const message = axios.isAxiosError(err) && err.response?.data?.message + ? err.response.data.message + : 'Ocurrió un error inesperado al eliminar el tipo de bobina.'; + setApiErrorMessage(message); + } + } + handleMenuClose(); + }; + + const handleMenuOpen = (event: React.MouseEvent, tipoBobina: TipoBobinaDto) => { + setAnchorEl(event.currentTarget); + setSelectedTipoBobinaRow(tipoBobina); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + setSelectedTipoBobinaRow(null); + }; + + const handleChangePage = (_event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const displayData = tiposBobina.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + if (!loading && !puedeVer) { + return ( + + Gestionar Tipos de Bobina + {error || "No tiene permiso para acceder a esta sección."} + + ); + } + + return ( + + + Gestionar Tipos de Bobina + + + + + setFiltroDenominacion(e.target.value)} + /> + {/* */} + + {puedeCrear && ( + + + + )} + + + {loading && } + {error && !loading && {error}} + {apiErrorMessage && {apiErrorMessage}} + + {!loading && !error && ( + + + + + Denominación + {(puedeModificar || puedeEliminar) && Acciones} + + + + {displayData.length === 0 && !loading ? ( + No se encontraron tipos de bobina. + ) : ( + displayData.map((tipo) => ( + + {tipo.denominacion} + {(puedeModificar || puedeEliminar) && ( + + handleMenuOpen(e, tipo)} + disabled={!puedeModificar && !puedeEliminar} + > + + + + )} + + )) + )} + +
+ +
+ )} + + + {puedeModificar && ( + { handleOpenModal(selectedTipoBobinaRow!); handleMenuClose(); }}> + Modificar + + )} + {puedeEliminar && ( + handleDelete(selectedTipoBobinaRow!.idTipoBobina)}> + Eliminar + + )} + {(!puedeModificar && !puedeEliminar) && Sin acciones} + + + setApiErrorMessage(null)} + /> +
+ ); +}; + +export default GestionarTiposBobinaPage; \ No newline at end of file diff --git a/Frontend/src/pages/Impresion/ImpresionIndexPage.tsx b/Frontend/src/pages/Impresion/ImpresionIndexPage.tsx new file mode 100644 index 0000000..38d083e --- /dev/null +++ b/Frontend/src/pages/Impresion/ImpresionIndexPage.tsx @@ -0,0 +1,71 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Tabs, Tab, Paper, Typography } from '@mui/material'; +import { Outlet, useNavigate, useLocation } from 'react-router-dom'; + +// Define las sub-pestañas del módulo Impresión +const impresionSubModules = [ + { label: 'Plantas', path: 'plantas' }, + { label: 'Tipos Bobina', path: 'tipos-bobina' }, + { label: 'Estados Bobina', path: 'estados-bobina' }, + // { label: 'Stock Bobinas', path: 'stock-bobinas' }, + // { label: 'Tiradas', path: 'tiradas' }, +]; + +const ImpresionIndexPage: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const [selectedSubTab, setSelectedSubTab] = useState(false); + + useEffect(() => { + const currentBasePath = '/impresion'; // Ruta base de este módulo + const subPath = location.pathname.startsWith(currentBasePath + '/') + ? location.pathname.substring(currentBasePath.length + 1) + : (location.pathname === currentBasePath ? impresionSubModules[0]?.path : undefined); + + const activeTabIndex = impresionSubModules.findIndex( + (subModule) => subModule.path === subPath + ); + + if (activeTabIndex !== -1) { + setSelectedSubTab(activeTabIndex); + } else { + if (location.pathname === currentBasePath && impresionSubModules.length > 0) { + navigate(impresionSubModules[0].path, { replace: true }); + setSelectedSubTab(0); + } else { + setSelectedSubTab(false); + } + } + }, [location.pathname, navigate]); + + const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { + setSelectedSubTab(newValue); + navigate(impresionSubModules[newValue].path); + }; + + return ( + + Módulo de Impresión + + + {impresionSubModules.map((subModule) => ( + + ))} + + + + {/* Renderizará GestionarPlantasPage, etc. */} + + + ); +}; + +export default ImpresionIndexPage; \ No newline at end of file diff --git a/Frontend/src/pages/LoginPage.tsx b/Frontend/src/pages/LoginPage.tsx index 4b10886..edaba4e 100644 --- a/Frontend/src/pages/LoginPage.tsx +++ b/Frontend/src/pages/LoginPage.tsx @@ -7,6 +7,8 @@ import type { LoginRequestDto } from '../models/dtos/LoginRequestDto'; // Usar t import { Container, TextField, Button, Typography, Box, Alert } from '@mui/material'; import authService from '../services/authService'; +import logo from '../assets/eldia.png'; + const LoginPage: React.FC = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -40,9 +42,25 @@ const LoginPage: React.FC = () => { return ( + {/* Contenedor responsive para la imagen */} + + Logo + = ({ children }) => { @@ -31,7 +37,7 @@ const ProtectedRoute: React.FC<{ children: JSX.Element }> = ({ children }) => { if (isLoading) return null; if (!isAuthenticated) { // console.log("ProtectedRoute: Not authenticated, redirecting to /login"); - return ; + return ; } // console.log("ProtectedRoute: Authenticated, rendering children"); return children; @@ -51,53 +57,61 @@ const PublicRoute: React.FC<{ children: JSX.Element }> = ({ children }) => { const MainLayoutWrapper: React.FC = () => ( - + ); // Placeholder simple const PlaceholderPage: React.FC<{ moduleName: string }> = ({ moduleName }) => ( - Página Principal del Módulo: {moduleName} + Página Principal del Módulo: {moduleName} ); const AppRoutes = () => { -return ( - - {/* Un solo de nivel superior */} - } /> + return ( + + {/* Un solo de nivel superior */} + } /> - {/* Rutas Protegidas que usan el MainLayout */} - - - - } - > + {/* Rutas Protegidas que usan el MainLayout */} + + + + } + > {/* Rutas hijas que se renderizarán en el Outlet de MainLayoutWrapper */} } /> {/* Para la ruta exacta "/" */} {/* Módulo de Distribución (anidado) */} }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> {/* Módulo Contable (anidado) */} }> - } /> - } /> - {/* Futuras sub-rutas de contables aquí */} + } /> + } /> + {/* Futuras sub-rutas de contables aquí */} + + + {/* Módulo de Impresión (anidado) */} + }> + } /> + } /> + } /> + } /> {/* Otros Módulos Principales (estos son "finales", no tienen más hijos) */} @@ -108,18 +122,18 @@ return ( {/* Ruta catch-all DENTRO del layout protegido */} } /> - {/* Cierre de la ruta padre "/" */} + {/* Cierre de la ruta padre "/" */} - {/* Podrías tener un catch-all global aquí si una ruta no coincide EN ABSOLUTO, + {/* Podrías tener un catch-all global aquí si una ruta no coincide EN ABSOLUTO, pero el path="*" dentro de la ruta "/" ya debería manejar la mayoría de los casos después de un login exitoso. Si un usuario no autenticado intenta una ruta inválida, ProtectedRoute lo manda a /login. */} - {/* } /> */} + {/* } /> */} - - -); + + + ); }; export default AppRoutes; \ No newline at end of file diff --git a/Frontend/src/services/empresaService.ts b/Frontend/src/services/empresaService.ts new file mode 100644 index 0000000..0081843 --- /dev/null +++ b/Frontend/src/services/empresaService.ts @@ -0,0 +1,46 @@ +import apiClient from './apiClient'; +import type { EmpresaDto } from '../models/dtos/Empresas/EmpresaDto'; +import type { CreateEmpresaDto } from '../models/dtos/Empresas/CreateEmpresaDto'; +import type { UpdateEmpresaDto } from '../models/dtos/Empresas/UpdateEmpresaDto'; + +const getAllEmpresas = async (nombreFilter?: string, detalleFilter?: string): Promise => { + const params: Record = {}; + if (nombreFilter) params.nombre = nombreFilter; + if (detalleFilter) params.detalle = detalleFilter; // Asegúrate que la API soporte esto + + // Llama a GET /api/empresas + const response = await apiClient.get('/empresas', { params }); + return response.data; +}; + +const getEmpresaById = async (id: number): Promise => { + // Llama a GET /api/empresas/{id} + const response = await apiClient.get(`/empresas/${id}`); + return response.data; +}; + +const createEmpresa = async (data: CreateEmpresaDto): Promise => { + // Llama a POST /api/empresas + const response = await apiClient.post('/empresas', data); + return response.data; // La API devuelve el objeto creado (201 Created) +}; + +const updateEmpresa = async (id: number, data: UpdateEmpresaDto): Promise => { + // Llama a PUT /api/empresas/{id} (204 No Content en éxito) + await apiClient.put(`/empresas/${id}`, data); +}; + +const deleteEmpresa = async (id: number): Promise => { + // Llama a DELETE /api/empresas/{id} (204 No Content en éxito) + await apiClient.delete(`/empresas/${id}`); +}; + +const empresaService = { + getAllEmpresas, + getEmpresaById, + createEmpresa, + updateEmpresa, + deleteEmpresa, +}; + +export default empresaService; \ No newline at end of file diff --git a/Frontend/src/services/estadoBobinaService.ts b/Frontend/src/services/estadoBobinaService.ts new file mode 100644 index 0000000..3c257c5 --- /dev/null +++ b/Frontend/src/services/estadoBobinaService.ts @@ -0,0 +1,40 @@ +import apiClient from './apiClient'; +import type { EstadoBobinaDto } from '../models/dtos/Impresion/EstadoBobinaDto'; +import type { CreateEstadoBobinaDto } from '../models/dtos/Impresion/CreateEstadoBobinaDto'; +import type { UpdateEstadoBobinaDto } from '../models/dtos/Impresion/UpdateEstadoBobinaDto'; + +const getAllEstadosBobina = async (denominacionFilter?: string): Promise => { + const params: Record = {}; + if (denominacionFilter) params.denominacion = denominacionFilter; + + const response = await apiClient.get('/estadosbobina', { params }); + return response.data; +}; + +const getEstadoBobinaById = async (id: number): Promise => { + const response = await apiClient.get(`/estadosbobina/${id}`); + return response.data; +}; + +const createEstadoBobina = async (data: CreateEstadoBobinaDto): Promise => { + const response = await apiClient.post('/estadosbobina', data); + return response.data; +}; + +const updateEstadoBobina = async (id: number, data: UpdateEstadoBobinaDto): Promise => { + await apiClient.put(`/estadosbobina/${id}`, data); +}; + +const deleteEstadoBobina = async (id: number): Promise => { + await apiClient.delete(`/estadosbobina/${id}`); +}; + +const estadoBobinaService = { + getAllEstadosBobina, + getEstadoBobinaById, + createEstadoBobina, + updateEstadoBobina, + deleteEstadoBobina, +}; + +export default estadoBobinaService; \ No newline at end of file diff --git a/Frontend/src/services/plantaService.ts b/Frontend/src/services/plantaService.ts new file mode 100644 index 0000000..23f0636 --- /dev/null +++ b/Frontend/src/services/plantaService.ts @@ -0,0 +1,46 @@ +import apiClient from './apiClient'; +import type { PlantaDto } from '../models/dtos/Impresion/PlantaDto'; +import type { CreatePlantaDto } from '../models/dtos/Impresion/CreatePlantaDto'; +import type { UpdatePlantaDto } from '../models/dtos/Impresion/UpdatePlantaDto'; + +const getAllPlantas = async (nombreFilter?: string, detalleFilter?: string): Promise => { + const params: Record = {}; + if (nombreFilter) params.nombre = nombreFilter; + if (detalleFilter) params.detalle = detalleFilter; // La API debe soportar esto + + // Llama a GET /api/plantas + const response = await apiClient.get('/plantas', { params }); + return response.data; +}; + +const getPlantaById = async (id: number): Promise => { + // Llama a GET /api/plantas/{id} + const response = await apiClient.get(`/plantas/${id}`); + return response.data; +}; + +const createPlanta = async (data: CreatePlantaDto): Promise => { + // Llama a POST /api/plantas + const response = await apiClient.post('/plantas', data); + return response.data; // La API devuelve el objeto creado (201 Created) +}; + +const updatePlanta = async (id: number, data: UpdatePlantaDto): Promise => { + // Llama a PUT /api/plantas/{id} (204 No Content en éxito) + await apiClient.put(`/plantas/${id}`, data); +}; + +const deletePlanta = async (id: number): Promise => { + // Llama a DELETE /api/plantas/{id} (204 No Content en éxito) + await apiClient.delete(`/plantas/${id}`); +}; + +const plantaService = { + getAllPlantas, + getPlantaById, + createPlanta, + updatePlanta, + deletePlanta, +}; + +export default plantaService; \ No newline at end of file diff --git a/Frontend/src/services/tipoBobinaService.ts b/Frontend/src/services/tipoBobinaService.ts new file mode 100644 index 0000000..3d61abc --- /dev/null +++ b/Frontend/src/services/tipoBobinaService.ts @@ -0,0 +1,45 @@ +import apiClient from './apiClient'; +import type { TipoBobinaDto } from '../models/dtos/Impresion/TipoBobinaDto'; +import type { CreateTipoBobinaDto } from '../models/dtos/Impresion/CreateTipoBobinaDto'; +import type { UpdateTipoBobinaDto } from '../models/dtos/Impresion/UpdateTipoBobinaDto'; + +const getAllTiposBobina = async (denominacionFilter?: string): Promise => { + const params: Record = {}; + if (denominacionFilter) params.denominacion = denominacionFilter; + + // Llama a GET /api/tiposbobina + const response = await apiClient.get('/tiposbobina', { params }); + return response.data; +}; + +const getTipoBobinaById = async (id: number): Promise => { + // Llama a GET /api/tiposbobina/{id} + const response = await apiClient.get(`/tiposbobina/${id}`); + return response.data; +}; + +const createTipoBobina = async (data: CreateTipoBobinaDto): Promise => { + // Llama a POST /api/tiposbobina + const response = await apiClient.post('/tiposbobina', data); + return response.data; // La API devuelve el objeto creado (201 Created) +}; + +const updateTipoBobina = async (id: number, data: UpdateTipoBobinaDto): Promise => { + // Llama a PUT /api/tiposbobina/{id} (204 No Content en éxito) + await apiClient.put(`/tiposbobina/${id}`, data); +}; + +const deleteTipoBobina = async (id: number): Promise => { + // Llama a DELETE /api/tiposbobina/{id} (204 No Content en éxito) + await apiClient.delete(`/tiposbobina/${id}`); +}; + +const tipoBobinaService = { + getAllTiposBobina, + getTipoBobinaById, + createTipoBobina, + updateTipoBobina, + deleteTipoBobina, +}; + +export default tipoBobinaService; \ No newline at end of file diff --git a/Frontend/src/services/zonaService.ts b/Frontend/src/services/zonaService.ts new file mode 100644 index 0000000..9837261 --- /dev/null +++ b/Frontend/src/services/zonaService.ts @@ -0,0 +1,46 @@ +import apiClient from './apiClient'; +import type { ZonaDto } from '../models/dtos/Zonas/ZonaDto'; // DTO para recibir listas +import type { CreateZonaDto } from '../models/dtos/Zonas/CreateZonaDto'; +import type { UpdateZonaDto } from '../models/dtos/Zonas/UpdateZonaDto'; + +const getAllZonas = async (nombreFilter?: string, descripcionFilter?: string): Promise => { + const params: Record = {}; + if (nombreFilter) params.nombre = nombreFilter; + if (descripcionFilter) params.descripcion = descripcionFilter; // Asegúrate que la API soporte este filtro si lo necesitas + + // Llama al GET /api/zonas (que por defecto devuelve solo activas según el servicio) + const response = await apiClient.get('/zonas', { params }); + return response.data; +}; + +const getZonaById = async (id: number): Promise => { + // Llama al GET /api/zonas/{id} (que por defecto devuelve solo activas según el servicio) + const response = await apiClient.get(`/zonas/${id}`); + return response.data; +}; + +const createZona = async (data: CreateZonaDto): Promise => { + // Llama a POST /api/zonas + const response = await apiClient.post('/zonas', data); + return response.data; // La API devuelve el objeto creado +}; + +const updateZona = async (id: number, data: UpdateZonaDto): Promise => { + // Llama a PUT /api/zonas/{id} + await apiClient.put(`/zonas/${id}`, data); +}; + +const deleteZona = async (id: number): Promise => { + // Llama a DELETE /api/zonas/{id} (que hará el soft delete) + await apiClient.delete(`/zonas/${id}`); +}; + +const zonaService = { + getAllZonas, + getZonaById, + createZona, + updateZona, + deleteZona, +}; + +export default zonaService;