Compare commits
30 Commits
Suscripcio
...
7e274ef114
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e274ef114 | |||
| 5212e31a03 | |||
| 9201d7222b | |||
| fc27b4b43e | |||
| 35e8d803b9 | |||
| 8e1b8d2326 | |||
| bc19e184aa | |||
| 29109cff13 | |||
| 7f1fadfc84 | |||
| 74f07df960 | |||
| 6ceb1477ae | |||
| c049c1e544 | |||
| 8c7278ceae | |||
| e8215f8586 | |||
| bf7d7c22ef | |||
| 2c584e9383 | |||
| e123dae182 | |||
| c27dc2a0ba | |||
| 24b1c07342 | |||
| cb64bbc1f5 | |||
| 057310ca47 | |||
| e95c851e5b | |||
| 038faefd35 | |||
| da50c052f1 | |||
| 5781713b13 | |||
| 9f8d577265 | |||
| b594a48fde | |||
| 2e7d1e36be | |||
| dd2277fce2 | |||
| 9412556fa8 |
@@ -26,12 +26,6 @@ jobs:
|
||||
set -e
|
||||
echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---"
|
||||
|
||||
# --- Asegurar que el Stack de la Base de Datos esté corriendo ---
|
||||
echo "Asegurando que el stack de la base de datos esté activo..."
|
||||
cd /opt/shared-services/database
|
||||
# El comando 'up -d' es idempotente. Si ya está corriendo, no hace nada.
|
||||
docker compose up -d
|
||||
|
||||
# 1. Preparar entorno
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
REPO_OWNER="dmolinari"
|
||||
|
||||
27
Backend/EliminacionLogicaDistribuidores.sql
Normal file
27
Backend/EliminacionLogicaDistribuidores.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
-- Script para agregar borrado lógico a Distribuidores
|
||||
|
||||
-- 1. Agregar columnas a la tabla principal
|
||||
ALTER TABLE dbo.dist_dtDistribuidores
|
||||
ADD Baja bit NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE dbo.dist_dtDistribuidores
|
||||
ADD FechaBaja datetime2(0) NULL;
|
||||
|
||||
-- 2. Agregar columnas a la tabla histórica
|
||||
ALTER TABLE dbo.dist_dtDistribuidores_H
|
||||
ADD Baja bit NULL;
|
||||
|
||||
ALTER TABLE dbo.dist_dtDistribuidores_H
|
||||
ADD FechaBaja datetime2(0) NULL;
|
||||
|
||||
-- 3. ATENCION: Actualizar Stored Procedures de Reportes
|
||||
-- Los siguientes Stored Procedures deben ser modificados para incluir la condicion "AND Baja = 0"
|
||||
-- en las consultas a "dist_dtDistribuidores":
|
||||
-- - SP_BalanceCuentaDistEntradaSalidaPorEmpresa
|
||||
-- - SP_BalanceCuentDistDebCredEmpresa
|
||||
-- - SP_BalanceCuentDistPagosEmpresa
|
||||
-- - SP_BalanceCuentSaldosEmpresas
|
||||
-- - SP_CantidadEntradaSalida
|
||||
-- - SP_CantidadEntradaSalidaCPromAgDia
|
||||
|
||||
PRINT 'Se agregaron correctamente las columnas Baja y FechaBaja a dist_dtDistribuidores y dist_dtDistribuidores_H';
|
||||
@@ -1,12 +0,0 @@
|
||||
# ================================================
|
||||
# VARIABLES DE ENTORNO PARA LA CONFIGURACIÓN DE CORREO
|
||||
# ================================================
|
||||
# El separador de doble guion bajo (__) se usa para mapear la jerarquía del JSON.
|
||||
# MailSettings:SmtpHost se convierte en MailSettings__SmtpHost
|
||||
|
||||
MailSettings__SmtpHost="mail.eldia.com"
|
||||
MailSettings__SmtpPort=587
|
||||
MailSettings__SenderName="Club - Diario El Día"
|
||||
MailSettings__SenderEmail="alertas@eldia.com"
|
||||
MailSettings__SmtpUser="alertas@eldia.com"
|
||||
MailSettings__SmtpPass="@Alertas713550@"
|
||||
@@ -40,19 +40,19 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<DistribuidorDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetAllDistribuidores([FromQuery] string? nombre, [FromQuery] string? nroDoc)
|
||||
public async Task<IActionResult> GetAllDistribuidores([FromQuery] string? nombre, [FromQuery] string? nroDoc, [FromQuery] bool? soloActivos = true)
|
||||
{
|
||||
if (!TienePermiso(PermisoVer)) return Forbid();
|
||||
var distribuidores = await _distribuidorService.ObtenerTodosAsync(nombre, nroDoc);
|
||||
var distribuidores = await _distribuidorService.ObtenerTodosAsync(nombre, nroDoc, soloActivos);
|
||||
return Ok(distribuidores);
|
||||
}
|
||||
|
||||
[HttpGet("dropdown")]
|
||||
[ProducesResponseType(typeof(IEnumerable<DistribuidorDropdownDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> GetAllDropdownDistribuidores()
|
||||
public async Task<IActionResult> GetAllDropdownDistribuidores([FromQuery] bool? soloActivos = true)
|
||||
{
|
||||
var distribuidores = await _distribuidorService.GetAllDropdownAsync();
|
||||
var distribuidores = await _distribuidorService.GetAllDropdownAsync(soloActivos);
|
||||
return Ok(distribuidores);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,27 @@ namespace GestionIntegral.Api.Controllers.Distribucion
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}/toggle-baja")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ToggleBajaDistribuidor(int id, [FromBody] ToggleBajaDistribuidorDto dto)
|
||||
{
|
||||
if (!TienePermiso(PermisoModificar)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (exito, error) = await _distribuidorService.ToggleBajaAsync(id, dto.DarDeBaja, dto.FechaBaja, userId.Value);
|
||||
if (!exito)
|
||||
{
|
||||
if (error == "Distribuidor no encontrado.") return NotFound(new { message = error });
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
|
||||
@@ -2,6 +2,7 @@ using GestionIntegral.Api.Dtos.Impresion;
|
||||
using GestionIntegral.Api.Services.Impresion;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -40,6 +41,7 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
||||
return null;
|
||||
}
|
||||
|
||||
// GET: api/stockbobinas
|
||||
// GET: api/stockbobinas
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<StockBobinaDto>), StatusCodes.Status200OK)]
|
||||
@@ -47,12 +49,23 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
||||
public async Task<IActionResult> GetAllStockBobinas(
|
||||
[FromQuery] int? idTipoBobina, [FromQuery] string? nroBobina, [FromQuery] int? idPlanta,
|
||||
[FromQuery] int? idEstadoBobina, [FromQuery] string? remito,
|
||||
[FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta)
|
||||
[FromQuery] DateTime? fechaDesde, [FromQuery] DateTime? fechaHasta,
|
||||
[FromQuery] DateTime? fechaEstadoDesde, [FromQuery] DateTime? fechaEstadoHasta) // <--- Nuevos parámetros agregados
|
||||
{
|
||||
if (!TienePermiso(PermisoVerStock)) return Forbid();
|
||||
try
|
||||
{
|
||||
var bobinas = await _stockBobinaService.ObtenerTodosAsync(idTipoBobina, nroBobina, idPlanta, idEstadoBobina, remito, fechaDesde, fechaHasta);
|
||||
var bobinas = await _stockBobinaService.ObtenerTodosAsync(
|
||||
idTipoBobina,
|
||||
nroBobina,
|
||||
idPlanta,
|
||||
idEstadoBobina,
|
||||
remito,
|
||||
fechaDesde,
|
||||
fechaHasta,
|
||||
fechaEstadoDesde,
|
||||
fechaEstadoHasta
|
||||
);
|
||||
return Ok(bobinas);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -172,5 +185,72 @@ namespace GestionIntegral.Api.Controllers.Impresion
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// GET: api/stockbobinas/verificar-remito
|
||||
[HttpGet("verificar-remito")]
|
||||
[ProducesResponseType(typeof(IEnumerable<StockBobinaDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
// CAMBIO: Hacer fechaRemito opcional (nullable)
|
||||
public async Task<IActionResult> VerificarRemito([FromQuery, BindRequired] int idPlanta, [FromQuery, BindRequired] string remito, [FromQuery] DateTime? fechaRemito)
|
||||
{
|
||||
if (!TienePermiso(PermisoIngresarBobina)) return Forbid();
|
||||
|
||||
try
|
||||
{
|
||||
// Pasamos el parámetro nullable al servicio
|
||||
var bobinasExistentes = await _stockBobinaService.VerificarRemitoExistenteAsync(idPlanta, remito, fechaRemito);
|
||||
return Ok(bobinasExistentes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error al verificar remito {Remito} para planta {IdPlanta}", remito, idPlanta);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Error interno al verificar el remito.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("actualizar-fecha-remito")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> ActualizarFechaRemitoLote([FromBody] UpdateFechaRemitoLoteDto dto)
|
||||
{
|
||||
// Reutilizamos el permiso de modificar datos, ya que es una corrección.
|
||||
if (!TienePermiso(PermisoModificarDatos)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (exito, error) = await _stockBobinaService.ActualizarFechaRemitoLoteAsync(dto, userId.Value);
|
||||
|
||||
if (!exito)
|
||||
{
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// POST: api/stockbobinas/lote
|
||||
[HttpPost("lote")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> IngresarLoteBobinas([FromBody] CreateStockBobinaLoteDto loteDto)
|
||||
{
|
||||
if (!TienePermiso(PermisoIngresarBobina)) return Forbid();
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
var userId = GetCurrentUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (exito, error) = await _stockBobinaService.IngresarBobinaLoteAsync(loteDto, userId.Value);
|
||||
|
||||
if (!exito)
|
||||
{
|
||||
return BadRequest(new { message = error });
|
||||
}
|
||||
|
||||
return NoContent(); // 204 es una buena respuesta para un lote procesado exitosamente sin devolver contenido.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,11 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
|
||||
void ComposeContent(IContainer container)
|
||||
{
|
||||
container.PaddingTop(1, Unit.Centimetre).Column(column =>
|
||||
// << CAMBIO: Reducido el padding superior de 1cm a 5mm >>
|
||||
container.PaddingTop(5, Unit.Millimetre).Column(column =>
|
||||
{
|
||||
column.Spacing(15);
|
||||
// << CAMBIO: Reducido el espaciado principal entre elementos de 15 a 10 puntos >>
|
||||
column.Spacing(10);
|
||||
|
||||
column.Item().Row(row =>
|
||||
{
|
||||
@@ -59,23 +61,24 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
});
|
||||
});
|
||||
|
||||
column.Item().PaddingTop(5).Border(1).Background(Colors.Grey.Lighten3).AlignCenter().Padding(2).Text(Model.NombreEmpresa).SemiBold();
|
||||
column.Item().PaddingTop(3).Border(1).Background(Colors.Grey.Lighten3).AlignCenter().Padding(2).Text(Model.NombreEmpresa).SemiBold();
|
||||
|
||||
column.Item().Border(1).Padding(10).Column(innerCol =>
|
||||
column.Item().Border(1).Padding(8).Column(innerCol => // << CAMBIO: Padding reducido de 10 a 8 >>
|
||||
{
|
||||
innerCol.Spacing(5);
|
||||
// << CAMBIO: Reducido el espaciado interno de 5 a 4 >>
|
||||
innerCol.Spacing(4);
|
||||
|
||||
// Fila de "Ingresados por Remito" con borde inferior sólido.
|
||||
innerCol.Item().BorderBottom(1, Unit.Point).BorderColor(Colors.Grey.Medium).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Text("Ingresados por Remito:").SemiBold();
|
||||
row.RelativeItem().AlignRight().Text(Model.TotalIngresadosPorRemito.ToString("N0"));
|
||||
}); // <-- SOLUCIÓN: Borde sólido simple.
|
||||
});
|
||||
|
||||
foreach (var item in Model.Detalles)
|
||||
{
|
||||
var totalSeccion = item.Devueltos - item.Llevados;
|
||||
innerCol.Item().PaddingTop(5).Row(row =>
|
||||
// << CAMBIO: Reducido el padding superior de 5 a 3 >>
|
||||
innerCol.Item().PaddingTop(3).Row(row =>
|
||||
{
|
||||
row.ConstantItem(100).Text(item.Tipo).SemiBold();
|
||||
|
||||
@@ -90,7 +93,8 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
r.RelativeItem().Text("Devueltos");
|
||||
r.RelativeItem().AlignRight().Text($"{item.Devueltos:N0}");
|
||||
});
|
||||
sub.Item().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(2).Row(r => {
|
||||
// << CAMBIO: Reducido el padding superior de 2 a 1 >>
|
||||
sub.Item().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(1).Row(r => {
|
||||
r.RelativeItem().Text(t => t.Span("Total").SemiBold());
|
||||
r.RelativeItem().AlignRight().Text(t => t.Span(totalSeccion.ToString("N0")).SemiBold());
|
||||
});
|
||||
@@ -99,7 +103,8 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
}
|
||||
});
|
||||
|
||||
column.Item().PaddingTop(10).Column(finalCol =>
|
||||
// << CAMBIO: Reducido el padding superior de 10 a 8 >>
|
||||
column.Item().PaddingTop(8).Column(finalCol =>
|
||||
{
|
||||
finalCol.Spacing(2);
|
||||
|
||||
@@ -112,13 +117,15 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
if (isBold) valueText.SemiBold();
|
||||
};
|
||||
|
||||
// Usamos bordes superiores para separar las líneas de total
|
||||
finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución a la Fecha", Model.TotalDevolucionALaFecha.ToString("N0"), false));
|
||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución Días Anteriores", Model.TotalDevolucionDiasAnteriores.ToString("N0"), false));
|
||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución", Model.TotalDevolucionGeneral.ToString("N0"), false));
|
||||
finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(5).Row(row => AddTotalRow(row, "Sin Cargo", Model.TotalSinCargo.ToString("N0"), false));
|
||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Sobrantes", $"-{Model.TotalSobrantes.ToString("N0")}", false));
|
||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(5).Row(row => AddTotalRow(row, "Diferencia", Model.DiferenciaFinal.ToString("N0"), true));
|
||||
// << CAMBIO: Reducido el padding superior de 2 a 1 en las siguientes líneas >>
|
||||
finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución a la Fecha", Model.TotalDevolucionALaFecha.ToString("N0"), false));
|
||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución Días Anteriores", Model.TotalDevolucionDiasAnteriores.ToString("N0"), false));
|
||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución", Model.TotalDevolucionGeneral.ToString("N0"), false));
|
||||
// << CAMBIO: Reducido el padding superior de 5 a 3 >>
|
||||
finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(3).Row(row => AddTotalRow(row, "Sin Cargo", Model.TotalSinCargo.ToString("N0"), false));
|
||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Sobrantes", $"-{Model.TotalSobrantes.ToString("N0")}", false));
|
||||
// << CAMBIO: Reducido el padding superior de 5 a 3 >>
|
||||
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(3).Row(row => AddTotalRow(row, "Diferencia", Model.DiferenciaFinal.ToString("N0"), true));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,22 +19,31 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
|
||||
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
|
||||
|
||||
// CORRECCIÓN: El método GetSettings ya no es necesario para este diseño.
|
||||
// La configuración por defecto es suficiente.
|
||||
// public DocumentSettings GetSettings() => DocumentSettings.Default;
|
||||
|
||||
public void Compose(IDocumentContainer container)
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A5);
|
||||
page.Margin(1, Unit.Centimetre);
|
||||
page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(9));
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(5, Unit.Millimetre);
|
||||
page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(9));
|
||||
|
||||
page.Header().Element(ComposeHeader);
|
||||
page.Content().Element(ComposeContent);
|
||||
});
|
||||
}
|
||||
page.Content().Column(mainColumn =>
|
||||
{
|
||||
mainColumn.Item()
|
||||
.AlignCenter()
|
||||
.Width(PageSizes.A6.Width)
|
||||
.Height(PageSizes.A6.Height)
|
||||
.Column(a6ContentColumn =>
|
||||
{
|
||||
a6ContentColumn.Item().PaddingRight(10, Unit.Millimetre).PaddingLeft(10, Unit.Millimetre).Column(content =>
|
||||
{
|
||||
content.Item().Element(ComposeHeader);
|
||||
content.Item().Element(ComposeContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void ComposeHeader(IContainer container)
|
||||
{
|
||||
|
||||
@@ -148,7 +148,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
|
||||
foreach (var item in Model.PromediosPorDia.OrderBy(d => dayOrder.GetValueOrDefault(d.Dia, 99)))
|
||||
{
|
||||
var porcDevolucion = item.Llevados > 0 ? (decimal)item.Devueltos * 100 / item.Llevados : 0;
|
||||
var porcDevolucion = item.Promedio_Llevados > 0 ? (decimal)item.Promedio_Devueltos * 100 / item.Promedio_Llevados : 0;
|
||||
|
||||
table.Cell().Border(1).Padding(3).Text(item.Dia);
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(item.Cant.ToString("N0"));
|
||||
@@ -162,7 +162,6 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
var general = Model.PromedioGeneral;
|
||||
if (general != null)
|
||||
{
|
||||
var porcDevolucionGeneral = general.Promedio_Llevados > 0 ? (decimal)general.Promedio_Devueltos * 100 / general.Promedio_Llevados : 0;
|
||||
var boldStyle = TextStyle.Default.SemiBold();
|
||||
|
||||
table.Cell().Border(1).Padding(3).Text(text => text.Span(general.Dia).Style(boldStyle));
|
||||
@@ -170,7 +169,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Llevados.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Devueltos.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(general.Promedio_Ventas.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(porcDevolucionGeneral.ToString("F2") + "%").Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(text => text.Span(Model.PorcentajeDevolucionGeneral.ToString("F2") + "%").Style(boldStyle));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
var llevados = item.Llevados ?? 0;
|
||||
var devueltos = item.Devueltos ?? 0;
|
||||
var ventaNetaDia = llevados - devueltos;
|
||||
if(llevados > 0)
|
||||
if (llevados > 0)
|
||||
{
|
||||
ventaNetaAcumulada += ventaNetaDia;
|
||||
conteoDias++;
|
||||
@@ -123,7 +123,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
|
||||
void ComposePromediosTable(IContainer container)
|
||||
{
|
||||
var dayOrder = new Dictionary<string, int> { { "Lunes", 1 }, { "Martes", 2 }, { "Miércoles", 3 }, { "Jueves", 4 }, { "Viernes", 5 }, { "Sábado", 6 }, { "Domingo", 7 }};
|
||||
var dayOrder = new Dictionary<string, int> { { "Lunes", 1 }, { "Martes", 2 }, { "Miércoles", 3 }, { "Jueves", 4 }, { "Viernes", 5 }, { "Sábado", 6 }, { "Domingo", 7 } };
|
||||
|
||||
container.Table(table =>
|
||||
{
|
||||
@@ -161,16 +161,16 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
|
||||
if (general != null)
|
||||
{
|
||||
var boldStyle = TextStyle.Default.SemiBold();
|
||||
var llevadosGeneral = general.Llevados ?? 0; // Usamos el total para el %
|
||||
var devueltosGeneral = general.Devueltos ?? 0; // Usamos el total para el %
|
||||
var porcDevolucionGeneral = llevadosGeneral > 0 ? (decimal)devueltosGeneral * 100 / llevadosGeneral : 0;
|
||||
var avgPercentage = Model.PromediosPorDia
|
||||
.Where(p => p.Dia != "General" && (p.Promedio_Llevados ?? 0) > 0)
|
||||
.Average(p => (decimal)(p.Promedio_Devueltos ?? 0) * 100 / (p.Promedio_Llevados ?? 1));
|
||||
|
||||
table.Cell().Border(1).Padding(3).Text(t => t.Span(general.Dia).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Cant?.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Llevados?.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Devueltos?.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(general.Promedio_Ventas?.ToString("N0")).Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(porcDevolucionGeneral.ToString("F2") + "%").Style(boldStyle));
|
||||
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(avgPercentage.ToString("F2") + "%").Style(boldStyle));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace GestionIntegral.Api.Controllers
|
||||
private const string PermisoVerReporteListadoDistMensual = "RR009";
|
||||
private const string PermisoVerReporteFacturasPublicidad = "RR010";
|
||||
private const string PermisoVerReporteDistSuscripciones = "RR011";
|
||||
private const string PermisoVerReportesSecretaria = "RR012";
|
||||
|
||||
public ReportesController(
|
||||
IReportesService reportesService,
|
||||
@@ -526,7 +527,7 @@ namespace GestionIntegral.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetVentaMensualSecretariaElDia([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002 para todos estos
|
||||
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002 para todos estos
|
||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta);
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Día'." });
|
||||
@@ -540,7 +541,7 @@ namespace GestionIntegral.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetVentaMensualSecretariaElDiaPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid();
|
||||
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid();
|
||||
|
||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta);
|
||||
|
||||
@@ -577,7 +578,7 @@ namespace GestionIntegral.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetVentaMensualSecretariaElPlata([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002
|
||||
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002
|
||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta);
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Plata'." });
|
||||
@@ -591,7 +592,7 @@ namespace GestionIntegral.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetVentaMensualSecretariaElPlataPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid();
|
||||
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid();
|
||||
|
||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta);
|
||||
|
||||
@@ -628,7 +629,7 @@ namespace GestionIntegral.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetVentaMensualSecretariaTirDevo([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002
|
||||
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002
|
||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta);
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de tirada/devolución." });
|
||||
@@ -642,7 +643,7 @@ namespace GestionIntegral.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetVentaMensualSecretariaTirDevoPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid();
|
||||
if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid();
|
||||
|
||||
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta);
|
||||
|
||||
@@ -677,13 +678,18 @@ namespace GestionIntegral.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetReporteDistribucionCanillasData([FromQuery] DateTime fecha, [FromQuery] int idEmpresa)
|
||||
public async Task<IActionResult> GetReporteDistribucionCanillasData(
|
||||
[FromQuery] DateTime fecha,
|
||||
[FromQuery] int idEmpresa,
|
||||
[FromQuery] bool? esAccionista
|
||||
)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid();
|
||||
|
||||
// Pasar el nuevo parámetro al servicio
|
||||
var (canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq,
|
||||
ctrlDevolucionesRemitos, ctrlDevolucionesParaDistCan, ctrlDevolucionesOtrosDias, error) =
|
||||
await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa);
|
||||
await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, esAccionista);
|
||||
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
|
||||
@@ -718,14 +724,20 @@ namespace GestionIntegral.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetReporteDistribucionCanillasPdf([FromQuery] DateTime fecha, [FromQuery] int idEmpresa, [FromQuery] bool soloTotales = false)
|
||||
public async Task<IActionResult> GetReporteDistribucionCanillasPdf(
|
||||
[FromQuery] DateTime fecha,
|
||||
[FromQuery] int idEmpresa,
|
||||
[FromQuery] bool? esAccionista,
|
||||
[FromQuery] bool soloTotales = false
|
||||
)
|
||||
{
|
||||
if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid();
|
||||
|
||||
// Pasar el nuevo parámetro al servicio
|
||||
var (
|
||||
canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq,
|
||||
remitos, ctrlDevoluciones, _, error
|
||||
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa);
|
||||
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, esAccionista);
|
||||
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
|
||||
@@ -794,11 +806,11 @@ namespace GestionIntegral.Api.Controllers
|
||||
_, // canillasAll
|
||||
_, // canillasFechaLiq
|
||||
_, // canillasAccFechaLiq
|
||||
ctrlDevolucionesRemitosData, // Para SP_ObtenerCtrlDevoluciones -> DataSet "DSObtenerCtrlDevoluciones"
|
||||
ctrlDevolucionesParaDistCanData, // Para SP_DistCanillasCantidadEntradaSalida -> DataSet "DSCtrlDevoluciones"
|
||||
ctrlDevolucionesOtrosDiasData, // Para SP_DistCanillasCantidadEntradaSalidaOtrosDias -> DataSet "DSCtrlDevolucionesOtrosDias"
|
||||
ctrlDevolucionesRemitosData,
|
||||
ctrlDevolucionesParaDistCanData,
|
||||
ctrlDevolucionesOtrosDiasData,
|
||||
error
|
||||
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); // Reutilizamos este método
|
||||
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, null);
|
||||
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
|
||||
@@ -832,7 +844,7 @@ namespace GestionIntegral.Api.Controllers
|
||||
var (
|
||||
_, _, _, _, _, // Datos no utilizados
|
||||
remitos, detalles, otrosDias, error
|
||||
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa);
|
||||
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, null);
|
||||
|
||||
if (error != null) return BadRequest(new { message = error });
|
||||
|
||||
|
||||
@@ -72,15 +72,16 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
|
||||
|
||||
[HttpGet("{anio:int}/{mes:int}")]
|
||||
public async Task<IActionResult> GetFacturas(
|
||||
int anio, int mes,
|
||||
[FromQuery] string? nombreSuscriptor,
|
||||
[FromQuery] string? estadoPago,
|
||||
[FromQuery] string? estadoFacturacion)
|
||||
int anio, int mes,
|
||||
[FromQuery] string? nombreSuscriptor,
|
||||
[FromQuery] string? estadoPago,
|
||||
[FromQuery] string? estadoFacturacion,
|
||||
[FromQuery] string? tipoFactura)
|
||||
{
|
||||
if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid();
|
||||
if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El período no es válido." });
|
||||
|
||||
var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion);
|
||||
var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion, tipoFactura);
|
||||
return Ok(resumenes);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
Task<PagoDistribuidor?> CreateAsync(PagoDistribuidor nuevoPago, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> UpdateAsync(PagoDistribuidor pagoAActualizar, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> DeleteAsync(int idPago, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> ExistsByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null);
|
||||
Task<PagoDistribuidor?> GetByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null);
|
||||
Task<IEnumerable<(PagoDistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
|
||||
@@ -70,9 +70,10 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null)
|
||||
public async Task<PagoDistribuidor?> GetByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.cue_PagosDistribuidor WHERE Recibo = @ReciboParam AND TipoMovimiento = @TipoMovParam");
|
||||
var sqlBuilder = new StringBuilder(SelectQueryBase()); // Reutiliza la consulta base
|
||||
sqlBuilder.Append(" WHERE Recibo = @ReciboParam AND TipoMovimiento = @TipoMovParam");
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("ReciboParam", recibo);
|
||||
parameters.Add("TipoMovParam", tipoMovimiento);
|
||||
@@ -85,12 +86,12 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
try
|
||||
{
|
||||
using var connection = _cf.CreateConnection();
|
||||
return await connection.ExecuteScalarAsync<bool>(sqlBuilder.ToString(), parameters);
|
||||
return await connection.QuerySingleOrDefaultAsync<PagoDistribuidor>(sqlBuilder.ToString(), parameters);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Error en ExistsByReciboAndTipoMovimientoAsync. Recibo: {Recibo}, Tipo: {Tipo}", recibo, tipoMovimiento);
|
||||
return true; // Asumir que existe en caso de error para prevenir duplicados
|
||||
_log.LogError(ex, "Error en GetByReciboAndTipoMovimientoAsync. Recibo: {Recibo}, Tipo: {Tipo}", recibo, tipoMovimiento);
|
||||
throw; // Relanzar para que el servicio lo maneje
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
|
||||
public async Task<IEnumerable<int>> GetAllDistribuidorIdsAsync()
|
||||
{
|
||||
var sql = "SELECT Id_Distribuidor FROM dbo.dist_dtDistribuidores";
|
||||
var sql = "SELECT Id_Distribuidor FROM dbo.dist_dtDistribuidores WHERE Baja = 0";
|
||||
try
|
||||
{
|
||||
using (var connection = _connectionFactory.CreateConnection())
|
||||
@@ -138,25 +138,45 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
|
||||
|
||||
public async Task<IEnumerable<Saldo>> GetSaldosParaGestionAsync(string? destinoFilter, int? idDestinoFilter, int? idEmpresaFilter)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder("SELECT Id_Saldo AS IdSaldo, Destino, Id_Destino AS IdDestino, Monto, Id_Empresa AS IdEmpresa, FechaUltimaModificacion FROM dbo.cue_Saldos WHERE 1=1");
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
SELECT Id_Saldo AS IdSaldo, Destino, Id_Destino AS IdDestino, Monto, Id_Empresa AS IdEmpresa, FechaUltimaModificacion
|
||||
FROM dbo.cue_Saldos s
|
||||
WHERE 1=1");
|
||||
|
||||
var parameters = new DynamicParameters();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(destinoFilter))
|
||||
{
|
||||
sqlBuilder.Append(" AND Destino = @Destino");
|
||||
sqlBuilder.Append(" AND s.Destino = @Destino");
|
||||
parameters.Add("Destino", destinoFilter);
|
||||
|
||||
// Filtro para excluir distribuidores de baja si el tipo es Distribuidores
|
||||
// No se aplica a Canillas por requerimiento explícito del usuario
|
||||
if (destinoFilter == "Distribuidores")
|
||||
{
|
||||
sqlBuilder.Append(" AND EXISTS (SELECT 1 FROM dbo.dist_dtDistribuidores d WHERE d.Id_Distribuidor = s.Id_Destino AND d.Baja = 0)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Si no hay filtro de destino, aplicamos el filtro de baja solo para Distribuidores
|
||||
sqlBuilder.Append(@" AND (
|
||||
(s.Destino = 'Distribuidores' AND EXISTS (SELECT 1 FROM dbo.dist_dtDistribuidores d WHERE d.Id_Distribuidor = s.Id_Destino AND d.Baja = 0))
|
||||
OR (s.Destino != 'Distribuidores')
|
||||
)");
|
||||
}
|
||||
|
||||
if (idDestinoFilter.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND Id_Destino = @IdDestino");
|
||||
sqlBuilder.Append(" AND s.Id_Destino = @IdDestino");
|
||||
parameters.Add("IdDestino", idDestinoFilter.Value);
|
||||
}
|
||||
if (idEmpresaFilter.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND Id_Empresa = @IdEmpresa");
|
||||
sqlBuilder.Append(" AND s.Id_Empresa = @IdEmpresa");
|
||||
parameters.Add("IdEmpresa", idEmpresaFilter.Value);
|
||||
}
|
||||
sqlBuilder.Append(" ORDER BY Destino, Id_Empresa, Id_Destino;");
|
||||
sqlBuilder.Append(" ORDER BY s.Destino, s.Id_Empresa, s.Id_Destino;");
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<Saldo>(sqlBuilder.ToString(), parameters);
|
||||
|
||||
@@ -22,12 +22,12 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter)
|
||||
public async Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
SELECT
|
||||
d.Id_Distribuidor AS IdDistribuidor, d.Nombre, d.Contacto, d.NroDoc, d.Id_Zona AS IdZona,
|
||||
d.Calle, d.Numero, d.Piso, d.Depto, d.Telefono, d.Email, d.Localidad,
|
||||
d.Calle, d.Numero, d.Piso, d.Depto, d.Telefono, d.Email, d.Localidad, d.Baja, d.FechaBaja,
|
||||
z.Nombre AS NombreZona
|
||||
FROM dbo.dist_dtDistribuidores d
|
||||
LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona
|
||||
@@ -44,6 +44,11 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
sqlBuilder.Append(" AND d.NroDoc LIKE @NroDocParam");
|
||||
parameters.Add("NroDocParam", $"%{nroDocFilter}%");
|
||||
}
|
||||
if (soloActivos.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND d.Baja = @BajaStatus ");
|
||||
parameters.Add("BajaStatus", !soloActivos.Value);
|
||||
}
|
||||
sqlBuilder.Append(" ORDER BY d.Nombre;");
|
||||
|
||||
try
|
||||
@@ -63,7 +68,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync()
|
||||
public async Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync(bool? soloActivos = true)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
SELECT
|
||||
@@ -71,6 +76,13 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
FROM dbo.dist_dtDistribuidores
|
||||
WHERE 1=1");
|
||||
var parameters = new DynamicParameters();
|
||||
|
||||
if (soloActivos.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND Baja = @BajaStatus ");
|
||||
parameters.Add("BajaStatus", !soloActivos.Value);
|
||||
}
|
||||
|
||||
sqlBuilder.Append(" ORDER BY Nombre;");
|
||||
try
|
||||
{
|
||||
@@ -92,7 +104,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
const string sql = @"
|
||||
SELECT
|
||||
d.Id_Distribuidor AS IdDistribuidor, d.Nombre, d.Contacto, d.NroDoc, d.Id_Zona AS IdZona,
|
||||
d.Calle, d.Numero, d.Piso, d.Depto, d.Telefono, d.Email, d.Localidad,
|
||||
d.Calle, d.Numero, d.Piso, d.Depto, d.Telefono, d.Email, d.Localidad, d.Baja, d.FechaBaja,
|
||||
z.Nombre AS NombreZona
|
||||
FROM dbo.dist_dtDistribuidores d
|
||||
LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona
|
||||
@@ -139,7 +151,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
const string sql = @"
|
||||
SELECT
|
||||
Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||
FROM dbo.dist_dtDistribuidores
|
||||
WHERE Id_Distribuidor = @IdParam";
|
||||
try
|
||||
@@ -223,10 +235,10 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
public async Task<Distribuidor?> CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction)
|
||||
{
|
||||
const string sqlInsert = @"
|
||||
INSERT INTO dbo.dist_dtDistribuidores (Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad)
|
||||
INSERT INTO dbo.dist_dtDistribuidores (Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja)
|
||||
OUTPUT INSERTED.Id_Distribuidor AS IdDistribuidor, INSERTED.Nombre, INSERTED.Contacto, INSERTED.NroDoc, INSERTED.Id_Zona AS IdZona,
|
||||
INSERTED.Calle, INSERTED.Numero, INSERTED.Piso, INSERTED.Depto, INSERTED.Telefono, INSERTED.Email, INSERTED.Localidad
|
||||
VALUES (@Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad);";
|
||||
INSERTED.Calle, INSERTED.Numero, INSERTED.Piso, INSERTED.Depto, INSERTED.Telefono, INSERTED.Email, INSERTED.Localidad, INSERTED.Baja, INSERTED.FechaBaja
|
||||
VALUES (@Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad, 0, NULL);";
|
||||
|
||||
var connection = transaction.Connection!;
|
||||
var inserted = await connection.QuerySingleAsync<Distribuidor>(sqlInsert, nuevoDistribuidor, transaction);
|
||||
@@ -234,8 +246,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
|
||||
const string sqlInsertHistorico = @"
|
||||
INSERT INTO dbo.dist_dtDistribuidores_H
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||
|
||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||
{
|
||||
@@ -251,6 +263,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
TelefonoParam = inserted.Telefono,
|
||||
EmailParam = inserted.Email,
|
||||
LocalidadParam = inserted.Localidad,
|
||||
BajaParam = inserted.Baja,
|
||||
FechaBajaParam = inserted.FechaBaja,
|
||||
IdUsuarioParam = idUsuario,
|
||||
FechaModParam = DateTime.Now,
|
||||
TipoModParam = "Creado"
|
||||
@@ -263,7 +277,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
var connection = transaction.Connection!;
|
||||
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
|
||||
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||
FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdDistribuidorParam",
|
||||
new { IdDistribuidorParam = distribuidorAActualizar.IdDistribuidor }, transaction);
|
||||
if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado.");
|
||||
@@ -275,8 +289,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
WHERE Id_Distribuidor = @IdDistribuidor;";
|
||||
const string sqlInsertHistorico = @"
|
||||
INSERT INTO dbo.dist_dtDistribuidores_H
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||
|
||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||
{
|
||||
@@ -292,6 +306,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
TelefonoParam = actual.Telefono,
|
||||
EmailParam = actual.Email,
|
||||
LocalidadParam = actual.Localidad,
|
||||
BajaParam = actual.Baja,
|
||||
FechaBajaParam = actual.FechaBaja,
|
||||
IdUsuarioParam = idUsuario,
|
||||
FechaModParam = DateTime.Now,
|
||||
TipoModParam = "Actualizado"
|
||||
@@ -306,7 +322,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
var connection = transaction.Connection!;
|
||||
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
|
||||
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||
FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdParam", new { IdParam = id }, transaction);
|
||||
if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado.");
|
||||
|
||||
@@ -314,8 +330,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
const string sqlDelete = "DELETE FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdParam";
|
||||
const string sqlInsertHistorico = @"
|
||||
INSERT INTO dbo.dist_dtDistribuidores_H
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
|
||||
|
||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||
{
|
||||
@@ -331,6 +347,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
TelefonoParam = actual.Telefono,
|
||||
EmailParam = actual.Email,
|
||||
LocalidadParam = actual.Localidad,
|
||||
BajaParam = actual.Baja,
|
||||
FechaBajaParam = actual.FechaBaja,
|
||||
IdUsuarioParam = idUsuario,
|
||||
FechaModParam = DateTime.Now,
|
||||
TipoModParam = "Eliminado"
|
||||
@@ -340,6 +358,47 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
return rowsAffected == 1;
|
||||
}
|
||||
|
||||
public async Task<bool> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario, IDbTransaction transaction)
|
||||
{
|
||||
var connection = transaction.Connection!;
|
||||
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
|
||||
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona,
|
||||
Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja
|
||||
FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdDistribuidorParam",
|
||||
new { IdDistribuidorParam = id }, transaction);
|
||||
if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado para dar de baja/alta.");
|
||||
|
||||
const string sqlUpdate = "UPDATE dbo.dist_dtDistribuidores SET Baja = @BajaParam, FechaBaja = @FechaBajaParam WHERE Id_Distribuidor = @IdDistribuidorParam;";
|
||||
const string sqlInsertHistorico = @"
|
||||
INSERT INTO dbo.dist_dtDistribuidores_H
|
||||
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Baja, FechaBaja, Id_Usuario, FechaMod, TipoMod)
|
||||
VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaNuevaParam, @FechaBajaNuevaParam, @IdUsuarioParam, @FechaModParam, @TipoModHistParam);";
|
||||
|
||||
await connection.ExecuteAsync(sqlInsertHistorico, new
|
||||
{
|
||||
IdDistribuidorParam = actual.IdDistribuidor,
|
||||
NombreParam = actual.Nombre,
|
||||
ContactoParam = actual.Contacto,
|
||||
NroDocParam = actual.NroDoc,
|
||||
IdZonaParam = actual.IdZona,
|
||||
CalleParam = actual.Calle,
|
||||
NumeroParam = actual.Numero,
|
||||
PisoParam = actual.Piso,
|
||||
DeptoParam = actual.Depto,
|
||||
TelefonoParam = actual.Telefono,
|
||||
EmailParam = actual.Email,
|
||||
LocalidadParam = actual.Localidad,
|
||||
BajaNuevaParam = darDeBaja,
|
||||
FechaBajaNuevaParam = (darDeBaja ? fechaBaja : null),
|
||||
IdUsuarioParam = idUsuario,
|
||||
FechaModParam = DateTime.Now,
|
||||
TipoModHistParam = (darDeBaja ? "Baja" : "Alta")
|
||||
}, transaction);
|
||||
|
||||
var rowsAffected = await connection.ExecuteAsync(sqlUpdate, new { BajaParam = darDeBaja, FechaBajaParam = (darDeBaja ? fechaBaja : null), IdDistribuidorParam = id }, transaction);
|
||||
return rowsAffected == 1;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(DistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
|
||||
@@ -72,6 +72,13 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// Inhabilitada la comprobacion de existencia previa por remito y tipo de movimiento
|
||||
// Pedido por Claudia Acosta el 18/11/2025
|
||||
// Motivo: El ex canillita Sergio Mazza opera como distribuidor y no utiliza remitos.
|
||||
// En el campo de remito se le asigna un numero aleatorio para cumplir con el requisito del sistema.
|
||||
// -------------------------------------------------------------------------------
|
||||
/*
|
||||
public async Task<bool> ExistsByRemitoAndTipoForPublicacionAsync(int remito, string tipoMovimiento, int idPublicacion, int? excludeIdParte = null)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.dist_EntradasSalidas WHERE Remito = @RemitoParam AND TipoMovimiento = @TipoMovimientoParam AND Id_Publicacion = @IdPublicacionParam");
|
||||
@@ -96,7 +103,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
return true; // Asumir que existe en caso de error para prevenir duplicados
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
public async Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction)
|
||||
{
|
||||
|
||||
@@ -8,16 +8,17 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
{
|
||||
public interface IDistribuidorRepository
|
||||
{
|
||||
Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter);
|
||||
Task<IEnumerable<(Distribuidor Distribuidor, string? NombreZona)>> GetAllAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true);
|
||||
Task<(Distribuidor? Distribuidor, string? NombreZona)> GetByIdAsync(int id);
|
||||
Task<Distribuidor?> GetByIdSimpleAsync(int id); // Para uso interno en el servicio
|
||||
Task<Distribuidor?> CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> UpdateAsync(Distribuidor distribuidorAActualizar, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> DeleteAsync(int id, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> ExistsByNroDocAsync(string nroDoc, int? excludeIdDistribuidor = null);
|
||||
Task<bool> ExistsByNameAsync(string nombre, int? excludeIdDistribuidor = null);
|
||||
Task<bool> IsInUseAsync(int id); // Verificar en dist_EntradasSalidas, cue_PagosDistribuidor, dist_PorcPago
|
||||
Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync();
|
||||
Task<IEnumerable<DistribuidorDropdownDto?>> GetAllDropdownAsync(bool? soloActivos = true);
|
||||
Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
|
||||
Task<IEnumerable<(DistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> UpdateAsync(EntradaSalidaDist esAActualizar, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> DeleteAsync(int idParte, int idUsuario, IDbTransaction transaction);
|
||||
Task<bool> ExistsByRemitoAndTipoForPublicacionAsync(int remito, string tipoMovimiento, int idPublicacion, int? excludeIdParte = null);
|
||||
//Task<bool> ExistsByRemitoAndTipoForPublicacionAsync(int remito, string tipoMovimiento, int idPublicacion, int? excludeIdParte = null);
|
||||
Task<IEnumerable<(EntradaSalidaDistHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
d.Nombre AS NombreDistribuidor
|
||||
FROM dbo.dist_PorcPago pp
|
||||
INNER JOIN dbo.dist_dtDistribuidores d ON pp.Id_Distribuidor = d.Id_Distribuidor
|
||||
WHERE pp.Id_Publicacion = @IdPublicacionParam
|
||||
WHERE pp.Id_Publicacion = @IdPublicacionParam AND d.Baja = 0
|
||||
ORDER BY d.Nombre, pp.VigenciaD DESC";
|
||||
try
|
||||
{
|
||||
|
||||
@@ -15,7 +15,9 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
int? idEstadoBobina,
|
||||
string? remitoFilter,
|
||||
DateTime? fechaDesde,
|
||||
DateTime? fechaHasta);
|
||||
DateTime? fechaHasta,
|
||||
DateTime? fechaEstadoDesde,
|
||||
DateTime? fechaEstadoHasta);
|
||||
|
||||
Task<StockBobina?> GetByIdAsync(int idBobina);
|
||||
Task<StockBobina?> GetByNroBobinaAsync(string nroBobina); // Para validar unicidad de NroBobina
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
|
||||
public async Task<IEnumerable<StockBobina>> GetAllAsync(
|
||||
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta)
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaEstadoDesde, DateTime? fechaEstadoHasta)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
SELECT
|
||||
@@ -69,6 +69,16 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
|
||||
sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam");
|
||||
parameters.Add("FechaHastaParam", fechaHasta.Value.Date);
|
||||
}
|
||||
if (fechaEstadoDesde.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND sb.FechaEstado >= @FechaEstadoDesdeParam");
|
||||
parameters.Add("FechaEstadoDesdeParam", fechaEstadoDesde.Value.Date);
|
||||
}
|
||||
if (fechaEstadoHasta.HasValue)
|
||||
{
|
||||
sqlBuilder.Append(" AND sb.FechaEstado <= @FechaEstadoHastaParam");
|
||||
parameters.Add("FechaEstadoHastaParam", fechaEstadoHasta.Value.Date);
|
||||
}
|
||||
|
||||
sqlBuilder.Append(" ORDER BY sb.FechaRemito DESC, sb.NroBobina;");
|
||||
|
||||
|
||||
@@ -48,5 +48,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
|
||||
Task<IEnumerable<FacturasParaReporteDto>> GetDatosReportePublicidadAsync(string periodo);
|
||||
Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesActivasAsync(DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesBajasAsync(DateTime fechaDesde, DateTime fechaHasta);
|
||||
Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(DateTime fecha);
|
||||
Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(DateTime fecha);
|
||||
}
|
||||
}
|
||||
@@ -653,5 +653,39 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
|
||||
return Enumerable.Empty<DistribucionSuscripcionDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(DateTime fecha)
|
||||
{
|
||||
const string spName = "dbo.SP_DistCanillasEntradaSalidaPubli_AllEmpresas";
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("@fecha", fecha, DbType.DateTime);
|
||||
try
|
||||
{
|
||||
using var connection = _dbConnectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<DetalleDistribucionCanillaDto>(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error SP {SPName}", spName);
|
||||
return Enumerable.Empty<DetalleDistribucionCanillaDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(DateTime fecha)
|
||||
{
|
||||
const string spName = "dbo.SP_DistCanillasAccEntradaSalidaPubli_AllEmpresas";
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("@fecha", fecha, DbType.DateTime);
|
||||
try
|
||||
{
|
||||
using var connection = _dbConnectionFactory.CreateConnection();
|
||||
return await connection.QueryAsync<DetalleDistribucionCanillaDto>(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error SP {SPName}", spName);
|
||||
return Enumerable.Empty<DetalleDistribucionCanillaDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,10 +59,15 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
{
|
||||
throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas.");
|
||||
}
|
||||
|
||||
const string sqlInsert = @"
|
||||
INSERT INTO dbo.susc_Facturas (IdSuscriptor, Periodo, FechaEmision, FechaVencimiento, ImporteBruto, DescuentoAplicado, ImporteFinal, EstadoPago, EstadoFacturacion)
|
||||
INSERT INTO dbo.susc_Facturas
|
||||
(IdSuscriptor, Periodo, FechaEmision, FechaVencimiento, ImporteBruto,
|
||||
DescuentoAplicado, ImporteFinal, EstadoPago, EstadoFacturacion, TipoFactura)
|
||||
OUTPUT INSERTED.*
|
||||
VALUES (@IdSuscriptor, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto, @DescuentoAplicado, @ImporteFinal, @EstadoPago, @EstadoFacturacion);";
|
||||
VALUES
|
||||
(@IdSuscriptor, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto,
|
||||
@DescuentoAplicado, @ImporteFinal, @EstadoPago, @EstadoFacturacion, @TipoFactura);";
|
||||
|
||||
return await transaction.Connection.QuerySingleAsync<Factura>(sqlInsert, nuevaFactura, transaction);
|
||||
}
|
||||
@@ -104,7 +109,8 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
return rowsAffected == idsFacturas.Count();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion)
|
||||
public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(
|
||||
string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura)
|
||||
{
|
||||
var sqlBuilder = new StringBuilder(@"
|
||||
WITH FacturaConEmpresa AS (
|
||||
@@ -149,6 +155,12 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
parameters.Add("EstadoFacturacion", estadoFacturacion);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tipoFactura))
|
||||
{
|
||||
sqlBuilder.Append(" AND f.TipoFactura = @TipoFactura");
|
||||
parameters.Add("TipoFactura", tipoFactura);
|
||||
}
|
||||
|
||||
sqlBuilder.Append(" ORDER BY s.NombreCompleto, f.IdFactura;");
|
||||
|
||||
try
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
|
||||
Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction);
|
||||
Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction);
|
||||
Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction);
|
||||
Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion);
|
||||
Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(
|
||||
string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura);
|
||||
Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction);
|
||||
Task<string?> GetUltimoPeriodoFacturadoAsync();
|
||||
Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>4923d7ee-0944-456c-abcd-d6ce13ba8485</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
||||
|
||||
@@ -14,5 +14,7 @@ namespace GestionIntegral.Api.Models.Distribucion
|
||||
public string? Telefono { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Localidad { get; set; }
|
||||
public bool Baja { get; set; } // Baja (bit, NOT NULL, DEFAULT 0)
|
||||
public DateTime? FechaBaja { get; set; } // FechaBaja (datetime2(0), NULL)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ namespace GestionIntegral.Api.Models.Distribucion
|
||||
public string? Telefono { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Localidad { get; set; }
|
||||
public bool? Baja { get; set; }
|
||||
public DateTime? FechaBaja { get; set; }
|
||||
public int Id_Usuario { get; set; }
|
||||
public DateTime FechaMod { get; set; }
|
||||
public string TipoMod { get; set; } = string.Empty;
|
||||
|
||||
@@ -15,5 +15,7 @@ namespace GestionIntegral.Api.Dtos.Distribucion
|
||||
public string? Telefono { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Localidad { get; set; }
|
||||
public bool Baja { get; set; }
|
||||
public DateTime? FechaBaja { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Distribucion
|
||||
{
|
||||
public class ToggleBajaDistribuidorDto
|
||||
{
|
||||
public bool DarDeBaja { get; set; }
|
||||
public DateTime? FechaBaja { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Backend/GestionIntegral.Api/Models/Dtos/Impresion/BobinaLoteDetalleDto.cs
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Impresion
|
||||
{
|
||||
public class BobinaLoteDetalleDto
|
||||
{
|
||||
public int IdTipoBobina { get; set; }
|
||||
public string NroBobina { get; set; } = string.Empty;
|
||||
public int Peso { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Backend/GestionIntegral.Api/Models/Dtos/Impresion/CreateStockBobinaLoteDto.cs
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Impresion
|
||||
{
|
||||
public class CreateStockBobinaLoteDto
|
||||
{
|
||||
[Required]
|
||||
public int IdPlanta { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(15)]
|
||||
public string Remito { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public DateTime FechaRemito { get; set; }
|
||||
|
||||
[Required]
|
||||
[MinLength(1, ErrorMessage = "Debe ingresar al menos una bobina.")]
|
||||
public List<BobinaLoteDetalleDto> Bobinas { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Backend/GestionIntegral.Api/Models/Dtos/Impresion/UpdateFechaRemitoLoteDto.cs
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GestionIntegral.Api.Dtos.Impresion
|
||||
{
|
||||
public class UpdateFechaRemitoLoteDto
|
||||
{
|
||||
[Required]
|
||||
public int IdPlanta { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string Remito { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime FechaRemitoActual { get; set; } // Para seguridad, nos aseguramos de cambiar el lote correcto
|
||||
|
||||
[Required]
|
||||
public DateTime NuevaFechaRemito { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,22 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public decimal PorcentajeDevolucionGeneral
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PromediosPorDia == null || !PromediosPorDia.Any()) return 0;
|
||||
|
||||
var totalPonderadoLlevados = PromediosPorDia.Sum(p => p.Promedio_Llevados * p.Cant);
|
||||
var totalPonderadoDevueltos = PromediosPorDia.Sum(p => p.Promedio_Devueltos * p.Cant);
|
||||
|
||||
if (totalPonderadoLlevados == 0) return 0;
|
||||
|
||||
// Calculamos el porcentaje usando los totales ponderados para máxima precisión como lo hace el frontend.
|
||||
return (decimal)totalPonderadoDevueltos * 100 / totalPonderadoLlevados;
|
||||
}
|
||||
}
|
||||
|
||||
// --- PROPIEDAD PARA LA FILA "GENERAL" ---
|
||||
public ListadoDistribucionCanillasPromedioDiaDto? PromedioGeneral
|
||||
{
|
||||
@@ -37,20 +53,27 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
||||
{
|
||||
if (PromediosPorDia == null || !PromediosPorDia.Any()) return null;
|
||||
|
||||
// Sumamos los totales, no promediamos los promedios
|
||||
var totalLlevados = PromediosPorDia.Sum(p => p.Llevados);
|
||||
var totalDevueltos = PromediosPorDia.Sum(p => p.Devueltos);
|
||||
// Sumamos los totales ponderados para cada columna
|
||||
var totalPonderadoLlevados = PromediosPorDia.Sum(p => p.Promedio_Llevados * p.Cant);
|
||||
var totalPonderadoDevueltos = PromediosPorDia.Sum(p => p.Promedio_Devueltos * p.Cant);
|
||||
var totalPonderadoVentas = PromediosPorDia.Sum(p => p.Promedio_Ventas * p.Cant);
|
||||
var totalDias = PromediosPorDia.Sum(p => p.Cant);
|
||||
|
||||
if (totalDias == 0) return null;
|
||||
|
||||
// Usamos Math.Round para un redondeo matemático estándar antes de la conversión.
|
||||
// MidpointRounding.AwayFromZero asegura que .5 se redondee hacia arriba, igual que en JavaScript.
|
||||
var promGeneralLlevados = (int)Math.Round((decimal)totalPonderadoLlevados / totalDias, MidpointRounding.AwayFromZero);
|
||||
var promGeneralDevueltos = (int)Math.Round((decimal)totalPonderadoDevueltos / totalDias, MidpointRounding.AwayFromZero);
|
||||
var promGeneralVentas = (int)Math.Round((decimal)totalPonderadoVentas / totalDias, MidpointRounding.AwayFromZero);
|
||||
|
||||
return new ListadoDistribucionCanillasPromedioDiaDto
|
||||
{
|
||||
Dia = "General",
|
||||
Cant = totalDias,
|
||||
Promedio_Llevados = totalLlevados / totalDias,
|
||||
Promedio_Devueltos = totalDevueltos / totalDias,
|
||||
Promedio_Ventas = (totalLlevados - totalDevueltos) / totalDias
|
||||
Promedio_Llevados = promGeneralLlevados,
|
||||
Promedio_Devueltos = promGeneralDevueltos,
|
||||
Promedio_Ventas = promGeneralVentas
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,26 +20,26 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (DetalleDiario == null || !DetalleDiario.Any())
|
||||
if (PromediosPorDia == null || !PromediosPorDia.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var diasConDatos = DetalleDiario.Count(d => (d.Llevados ?? 0) > 0);
|
||||
if (diasConDatos == 0) return null;
|
||||
|
||||
var totalLlevados = DetalleDiario.Sum(d => d.Llevados ?? 0);
|
||||
var totalDevueltos = DetalleDiario.Sum(d => d.Devueltos ?? 0);
|
||||
var promediosValidos = PromediosPorDia.Where(p => p.Dia != "General").ToList();
|
||||
if (!promediosValidos.Any()) return null;
|
||||
var countPromedios = promediosValidos.Count;
|
||||
var sumPromLlevados = promediosValidos.Sum(p => p.Promedio_Llevados ?? 0);
|
||||
var sumPromDevueltos = promediosValidos.Sum(p => p.Promedio_Devueltos ?? 0);
|
||||
var sumPromVentas = promediosValidos.Sum(p => p.Promedio_Ventas ?? 0);
|
||||
|
||||
return new ListadoDistribucionDistPromedioDiaDto
|
||||
{
|
||||
Dia = "General",
|
||||
Cant = diasConDatos,
|
||||
Promedio_Llevados = totalLlevados / diasConDatos,
|
||||
Promedio_Devueltos = totalDevueltos / diasConDatos,
|
||||
Promedio_Ventas = (totalLlevados - totalDevueltos) / diasConDatos,
|
||||
Llevados = totalLlevados, // Guardamos el total para el cálculo del %
|
||||
Devueltos = totalDevueltos // Guardamos el total para el cálculo del %
|
||||
Cant = promediosValidos.Sum(p => p.Cant ?? 0),
|
||||
Promedio_Llevados = (int)Math.Round((decimal)sumPromLlevados / countPromedios, MidpointRounding.AwayFromZero),
|
||||
Promedio_Devueltos = (int)Math.Round((decimal)sumPromDevueltos / countPromedios, MidpointRounding.AwayFromZero),
|
||||
Promedio_Ventas = (int)Math.Round((decimal)sumPromVentas / countPromedios, MidpointRounding.AwayFromZero),
|
||||
Llevados = (int)Math.Round((decimal)sumPromLlevados / countPromedios, MidpointRounding.AwayFromZero),
|
||||
Devueltos = (int)Math.Round((decimal)sumPromDevueltos / countPromedios, MidpointRounding.AwayFromZero)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace GestionIntegral.Api.Dtos.Suscripciones
|
||||
public string EstadoPago { get; set; } = string.Empty;
|
||||
public string EstadoFacturacion { get; set; } = string.Empty;
|
||||
public string? NumeroFactura { get; set; }
|
||||
public decimal TotalPagado { get; set; }
|
||||
public string TipoFactura { get; set; } = string.Empty;
|
||||
public int IdSuscriptor { get; set; }
|
||||
public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>();
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,6 @@ namespace GestionIntegral.Api.Models.Suscripciones
|
||||
public string? NumeroFactura { get; set; }
|
||||
public int? IdLoteDebito { get; set; }
|
||||
public string? MotivoRechazo { get; set; }
|
||||
public string TipoFactura { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,6 @@ using GestionIntegral.Api.Models.Comunicaciones;
|
||||
using GestionIntegral.Api.Services.Comunicaciones;
|
||||
using GestionIntegral.Api.Data.Repositories.Comunicaciones;
|
||||
|
||||
// Carga las variables de entorno desde el archivo .env al inicio de la aplicación.
|
||||
// Debe ser la primera línea para que la configuración esté disponible para el 'builder'.
|
||||
DotNetEnv.Env.Load();
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// --- Registros de Servicios ---
|
||||
|
||||
@@ -22,13 +22,16 @@ namespace GestionIntegral.Api.Services.Anomalia
|
||||
public async Task<IEnumerable<AlertaGenericaDto>> ObtenerAlertasNoLeidasAsync()
|
||||
{
|
||||
// Apunta a la nueva tabla genérica 'Sistema_Alertas'
|
||||
var query = "SELECT * FROM Sistema_Alertas WHERE Leida = 0 ORDER BY FechaDeteccion DESC";
|
||||
//var query = "SELECT * FROM Sistema_Alertas WHERE Leida = 0 ORDER BY FechaDeteccion DESC";
|
||||
try
|
||||
{
|
||||
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||
{
|
||||
/*
|
||||
var alertas = await connection.QueryAsync<AlertaGenericaDto>(query);
|
||||
return alertas ?? Enumerable.Empty<AlertaGenericaDto>();
|
||||
*/
|
||||
return Enumerable.Empty<AlertaGenericaDto>();
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
@@ -40,17 +43,20 @@ namespace GestionIntegral.Api.Services.Anomalia
|
||||
|
||||
public async Task<(bool Exito, string? Error)> MarcarComoLeidaAsync(int idAlerta)
|
||||
{
|
||||
var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE IdAlerta = @IdAlerta";
|
||||
//var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE IdAlerta = @IdAlerta";
|
||||
try
|
||||
{
|
||||
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||
{
|
||||
/*
|
||||
var result = await connection.ExecuteAsync(query, new { IdAlerta = idAlerta });
|
||||
if (result > 0)
|
||||
{
|
||||
return (true, null);
|
||||
}
|
||||
return (false, "La alerta no fue encontrada o ya estaba marcada.");
|
||||
*/
|
||||
return (true, null); // Retornar éxito silencioso por ahora
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
@@ -62,15 +68,18 @@ namespace GestionIntegral.Api.Services.Anomalia
|
||||
|
||||
public async Task<(bool Exito, string? Error)> MarcarGrupoComoLeidoAsync(string tipoAlerta, int idEntidad)
|
||||
{
|
||||
var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE TipoAlerta = @TipoAlerta AND IdEntidad = @IdEntidad AND Leida = 0";
|
||||
//var query = "UPDATE Sistema_Alertas SET Leida = 1 WHERE TipoAlerta = @TipoAlerta AND IdEntidad = @IdEntidad AND Leida = 0";
|
||||
try
|
||||
{
|
||||
using (var connection = _dbConnectionFactory.CreateConnection())
|
||||
{
|
||||
/*
|
||||
var result = await connection.ExecuteAsync(query, new { TipoAlerta = tipoAlerta, IdEntidad = idEntidad });
|
||||
// No es un error si no se actualizan filas (puede que no hubiera ninguna para ese grupo)
|
||||
_logger.LogInformation("Marcadas como leídas {Count} alertas para Tipo: {Tipo}, EntidadID: {IdEntidad}", result, tipoAlerta, idEntidad);
|
||||
return (true, null);
|
||||
*/
|
||||
return (true, null);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
|
||||
@@ -4,6 +4,8 @@ using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MimeKit;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
{
|
||||
@@ -88,6 +90,30 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
using var smtp = new SmtpClient();
|
||||
try
|
||||
{
|
||||
// Se añade una política de validación de certificado personalizada.
|
||||
// Esto es necesario para entornos de desarrollo o redes internas donde
|
||||
// el nombre del host al que nos conectamos (ej. una IP) no coincide
|
||||
// con el nombre en el certificado SSL (ej. mail.eldia.com).
|
||||
smtp.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||
{
|
||||
// Si no hay errores, el certificado es válido.
|
||||
if (sslPolicyErrors == SslPolicyErrors.None)
|
||||
return true;
|
||||
|
||||
// Si el único error es que el nombre no coincide (RemoteCertificateNameMismatch)
|
||||
// Y el certificado es el que esperamos (emitido para "mail.eldia.com"),
|
||||
// entonces lo aceptamos como válido.
|
||||
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch) && certificate != null && certificate.Subject.Contains("CN=mail.eldia.com"))
|
||||
{
|
||||
_logger.LogWarning("Se aceptó un certificado SSL con 'Name Mismatch' para el host de confianza 'mail.eldia.com'.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Para cualquier otro error, rechazamos el certificado.
|
||||
_logger.LogError("Error de validación de certificado SSL: {Errors}", sslPolicyErrors);
|
||||
return false;
|
||||
};
|
||||
|
||||
await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls);
|
||||
await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass);
|
||||
await smtp.SendAsync(emailMessage);
|
||||
@@ -95,20 +121,6 @@ namespace GestionIntegral.Api.Services.Comunicaciones
|
||||
log.Estado = "Enviado";
|
||||
_logger.LogInformation("Email enviado exitosamente a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
||||
}
|
||||
catch (SmtpCommandException scEx)
|
||||
{
|
||||
_logger.LogError(scEx, "Error de comando SMTP al enviar a {Destinatario}. StatusCode: {StatusCode}", destinatario, scEx.StatusCode);
|
||||
log.Estado = "Fallido";
|
||||
log.Error = $"Error del servidor: ({scEx.StatusCode}) {scEx.Message}";
|
||||
throw;
|
||||
}
|
||||
catch (AuthenticationException authEx)
|
||||
{
|
||||
_logger.LogError(authEx, "Error de autenticación con el servidor SMTP.");
|
||||
log.Estado = "Fallido";
|
||||
log.Error = "Error de autenticación. Revise las credenciales de correo.";
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error general al enviar email a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
|
||||
|
||||
@@ -93,8 +93,18 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
return (null, "Tipo de pago no válido.");
|
||||
if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null)
|
||||
return (null, "Empresa no válida.");
|
||||
if (await _pagoRepo.ExistsByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento))
|
||||
return (null, $"Ya existe un pago '{createDto.TipoMovimiento}' con el número de recibo '{createDto.Recibo}'.");
|
||||
var pagoExistente = await _pagoRepo.GetByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento);
|
||||
if (pagoExistente != null)
|
||||
{
|
||||
// Si encontramos un duplicado, obtenemos los detalles para el mensaje de error
|
||||
var distribuidor = await _distribuidorRepo.GetByIdSimpleAsync(pagoExistente.IdDistribuidor);
|
||||
var empresa = await _empresaRepo.GetByIdAsync(pagoExistente.IdEmpresa);
|
||||
|
||||
string mensajeError = $"El recibo N° {createDto.Recibo} ya fue registrado como '{pagoExistente.TipoMovimiento}' el {pagoExistente.Fecha:dd/MM/yyyy} " +
|
||||
$"para el distribuidor '{distribuidor?.Nombre ?? "Desconocido"}' en la empresa '{empresa?.Nombre ?? "Desconocida"}'.";
|
||||
|
||||
return (null, mensajeError);
|
||||
}
|
||||
|
||||
var nuevoPago = new PagoDistribuidor
|
||||
{
|
||||
@@ -275,25 +285,25 @@ namespace GestionIntegral.Api.Services.Contables
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
int? idPagoAfectado)
|
||||
{
|
||||
var historialData = await _pagoRepo.GetHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idPagoAfectado);
|
||||
|
||||
return historialData.Select(h => new PagoDistribuidorHistorialDto
|
||||
{
|
||||
Id_Pago = h.Historial.Id_Pago,
|
||||
Id_Distribuidor = h.Historial.Id_Distribuidor,
|
||||
Fecha = h.Historial.Fecha,
|
||||
TipoMovimiento = h.Historial.TipoMovimiento,
|
||||
Recibo = h.Historial.Recibo,
|
||||
Monto = h.Historial.Monto,
|
||||
Id_TipoPago = h.Historial.Id_TipoPago,
|
||||
Detalle = h.Historial.Detalle,
|
||||
Id_Empresa = h.Historial.Id_Empresa,
|
||||
Id_Usuario = h.Historial.Id_Usuario,
|
||||
NombreUsuarioModifico = h.NombreUsuarioModifico,
|
||||
FechaMod = h.Historial.FechaMod,
|
||||
TipoMod = h.Historial.TipoMod
|
||||
}).ToList();
|
||||
}
|
||||
var historialData = await _pagoRepo.GetHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idPagoAfectado);
|
||||
|
||||
return historialData.Select(h => new PagoDistribuidorHistorialDto
|
||||
{
|
||||
Id_Pago = h.Historial.Id_Pago,
|
||||
Id_Distribuidor = h.Historial.Id_Distribuidor,
|
||||
Fecha = h.Historial.Fecha,
|
||||
TipoMovimiento = h.Historial.TipoMovimiento,
|
||||
Recibo = h.Historial.Recibo,
|
||||
Monto = h.Historial.Monto,
|
||||
Id_TipoPago = h.Historial.Id_TipoPago,
|
||||
Detalle = h.Historial.Detalle,
|
||||
Id_Empresa = h.Historial.Id_Empresa,
|
||||
Id_Usuario = h.Historial.Id_Usuario,
|
||||
NombreUsuarioModifico = h.NombreUsuarioModifico,
|
||||
FechaMod = h.Historial.FechaMod,
|
||||
TipoMod = h.Historial.TipoMod
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,20 +56,22 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
Depto = data.Distribuidor.Depto,
|
||||
Telefono = data.Distribuidor.Telefono,
|
||||
Email = data.Distribuidor.Email,
|
||||
Localidad = data.Distribuidor.Localidad
|
||||
Localidad = data.Distribuidor.Localidad,
|
||||
Baja = data.Distribuidor.Baja,
|
||||
FechaBaja = data.Distribuidor.FechaBaja
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter)
|
||||
public async Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true)
|
||||
{
|
||||
var data = await _distribuidorRepository.GetAllAsync(nombreFilter, nroDocFilter);
|
||||
var data = await _distribuidorRepository.GetAllAsync(nombreFilter, nroDocFilter, soloActivos);
|
||||
// Filtrar nulos y asegurar al compilador que no hay nulos en la lista final
|
||||
return data.Select(MapToDto).Where(dto => dto != null).Select(dto => dto!);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync()
|
||||
public async Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync(bool? soloActivos = true)
|
||||
{
|
||||
var data = await _distribuidorRepository.GetAllDropdownAsync();
|
||||
var data = await _distribuidorRepository.GetAllDropdownAsync(soloActivos);
|
||||
// Asegurar que el resultado no sea nulo y no contiene elementos nulos
|
||||
if (data == null)
|
||||
{
|
||||
@@ -223,6 +225,31 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario)
|
||||
{
|
||||
var distribuidorExistente = await _distribuidorRepository.GetByIdSimpleAsync(id);
|
||||
if (distribuidorExistente == null) return (false, "Distribuidor no encontrado.");
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
var toggled = await _distribuidorRepository.ToggleBajaAsync(id, darDeBaja, fechaBaja, idUsuario, transaction);
|
||||
if (!toggled) throw new DataException("Error al cambiar estado de baja.");
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Distribuidor ID {IdDistribuidor} dado de {Estado} por Usuario ID {IdUsuario}.", id, darDeBaja ? "baja" : "alta", idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Distribuidor no encontrado."); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error ToggleBajaAsync Distribuidor ID: {IdDistribuidor}", id);
|
||||
return (false, $"Error interno: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<DistribuidorHistorialDto>> ObtenerHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
|
||||
@@ -167,10 +167,11 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(createDto.IdDistribuidor);
|
||||
if (distribuidor == null) return (null, "Distribuidor no válido.");
|
||||
|
||||
/*
|
||||
if (await _esRepository.ExistsByRemitoAndTipoForPublicacionAsync(createDto.Remito, createDto.TipoMovimiento, createDto.IdPublicacion))
|
||||
{
|
||||
return (null, $"Ya existe un movimiento de '{createDto.TipoMovimiento}' con el remito N°{createDto.Remito} para esta publicación.");
|
||||
}
|
||||
}*/
|
||||
|
||||
// Determinar IDs de Precio, Recargo y Porcentaje activos en la fecha del movimiento
|
||||
var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(createDto.IdPublicacion, createDto.Fecha.Date);
|
||||
|
||||
@@ -7,12 +7,13 @@ namespace GestionIntegral.Api.Services.Distribucion
|
||||
{
|
||||
public interface IDistribuidorService
|
||||
{
|
||||
Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter);
|
||||
Task<IEnumerable<DistribuidorDto>> ObtenerTodosAsync(string? nombreFilter, string? nroDocFilter, bool? soloActivos = true);
|
||||
Task<DistribuidorDto?> ObtenerPorIdAsync(int id);
|
||||
Task<(DistribuidorDto? Distribuidor, string? Error)> CrearAsync(CreateDistribuidorDto createDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarAsync(int id, UpdateDistribuidorDto updateDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> EliminarAsync(int id, int idUsuario);
|
||||
Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync();
|
||||
Task<(bool Exito, string? Error)> ToggleBajaAsync(int id, bool darDeBaja, DateTime? fechaBaja, int idUsuario);
|
||||
Task<IEnumerable<DistribuidorDropdownDto>> GetAllDropdownAsync(bool? soloActivos = true);
|
||||
Task<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
|
||||
Task<IEnumerable<DistribuidorHistorialDto>> ObtenerHistorialAsync(
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
{
|
||||
Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
|
||||
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta);
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaEstadoDesde, DateTime? fechaEstadoHasta);
|
||||
|
||||
Task<StockBobinaDto?> ObtenerPorIdAsync(int idBobina);
|
||||
Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario);
|
||||
@@ -21,5 +21,8 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
DateTime? fechaDesde, DateTime? fechaHasta,
|
||||
int? idUsuarioModifico, string? tipoModificacion,
|
||||
int? idBobinaAfectada, int? idTipoBobinaFiltro, int? idPlantaFiltro, int? idEstadoBobinaFiltro);
|
||||
Task<IEnumerable<StockBobinaDto>> VerificarRemitoExistenteAsync(int idPlanta, string remito, DateTime? fechaRemito);
|
||||
Task<(bool Exito, string? Error)> IngresarBobinaLoteAsync(CreateStockBobinaLoteDto loteDto, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarFechaRemitoLoteAsync(UpdateFechaRemitoLoteDto dto, int idUsuario);
|
||||
}
|
||||
}
|
||||
@@ -85,9 +85,9 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
|
||||
public async Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
|
||||
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta,
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta)
|
||||
int? idEstadoBobina, string? remitoFilter, DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaEstadoDesde, DateTime? fechaEstadoHasta)
|
||||
{
|
||||
var bobinas = await _stockBobinaRepository.GetAllAsync(idTipoBobina, nroBobinaFilter, idPlanta, idEstadoBobina, remitoFilter, fechaDesde, fechaHasta);
|
||||
var bobinas = await _stockBobinaRepository.GetAllAsync(idTipoBobina, nroBobinaFilter, idPlanta, idEstadoBobina, remitoFilter, fechaDesde, fechaHasta, fechaEstadoDesde, fechaEstadoHasta);
|
||||
var dtos = new List<StockBobinaDto>();
|
||||
foreach (var bobina in bobinas)
|
||||
{
|
||||
@@ -166,16 +166,16 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
}
|
||||
if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null)
|
||||
return (false, "Tipo de bobina inválido.");
|
||||
if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null)
|
||||
return (false, "Planta inválida.");
|
||||
//if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null)
|
||||
// return (false, "Planta inválida.");
|
||||
|
||||
|
||||
bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina;
|
||||
bobinaExistente.NroBobina = updateDto.NroBobina;
|
||||
bobinaExistente.Peso = updateDto.Peso;
|
||||
bobinaExistente.IdPlanta = updateDto.IdPlanta;
|
||||
bobinaExistente.Remito = updateDto.Remito;
|
||||
bobinaExistente.FechaRemito = updateDto.FechaRemito.Date;
|
||||
//bobinaExistente.IdPlanta = updateDto.IdPlanta;
|
||||
//bobinaExistente.Remito = updateDto.Remito;
|
||||
//bobinaExistente.FechaRemito = updateDto.FechaRemito.Date;
|
||||
// FechaEstado se mantiene ya que el estado no cambia aquí
|
||||
|
||||
var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados");
|
||||
@@ -199,13 +199,21 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
using var transaction = connection.BeginTransaction();
|
||||
try
|
||||
{
|
||||
var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de la transacción
|
||||
var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina);
|
||||
if (bobina == null)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
return (false, "Bobina no encontrada.");
|
||||
}
|
||||
|
||||
// Comparamos solo las fechas (sin hora) para evitar problemas de precisión.
|
||||
if (cambiarEstadoDto.FechaCambioEstado.Date < bobina.FechaRemito.Date)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
return (false, $"Error de integridad: La fecha del nuevo estado ({cambiarEstadoDto.FechaCambioEstado:dd/MM/yyyy}) " +
|
||||
$"no puede ser anterior a la fecha de ingreso por remito ({bobina.FechaRemito:dd/MM/yyyy}).");
|
||||
}
|
||||
|
||||
var nuevoEstado = await _estadoBobinaRepository.GetByIdAsync(cambiarEstadoDto.NuevoEstadoId);
|
||||
if (nuevoEstado == null)
|
||||
{
|
||||
@@ -383,5 +391,153 @@ namespace GestionIntegral.Api.Services.Impresion
|
||||
TipoMod = h.Historial.TipoMod
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<StockBobinaDto>> VerificarRemitoExistenteAsync(int idPlanta, string remito, DateTime? fechaRemito)
|
||||
{
|
||||
// Si la fecha tiene valor, filtramos por ese día exacto. Si no, busca en cualquier fecha.
|
||||
DateTime? fechaDesde = fechaRemito?.Date;
|
||||
DateTime? fechaHasta = fechaRemito?.Date;
|
||||
|
||||
var bobinas = await _stockBobinaRepository.GetAllAsync(null, null, idPlanta, null, remito, fechaDesde, fechaHasta, null, null);
|
||||
|
||||
var dtos = new List<StockBobinaDto>();
|
||||
foreach (var bobina in bobinas)
|
||||
{
|
||||
dtos.Add(await MapToDto(bobina));
|
||||
}
|
||||
return dtos;
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> ActualizarFechaRemitoLoteAsync(UpdateFechaRemitoLoteDto dto, int idUsuario)
|
||||
{
|
||||
// 1. Buscar todas las bobinas que coinciden con el lote a modificar.
|
||||
var bobinasAActualizar = await _stockBobinaRepository.GetAllAsync(
|
||||
idTipoBobina: null,
|
||||
nroBobinaFilter: null,
|
||||
idPlanta: dto.IdPlanta,
|
||||
idEstadoBobina: null,
|
||||
remitoFilter: dto.Remito,
|
||||
fechaDesde: dto.FechaRemitoActual.Date,
|
||||
fechaHasta: dto.FechaRemitoActual.Date,
|
||||
fechaEstadoDesde: null,
|
||||
fechaEstadoHasta: null
|
||||
);
|
||||
|
||||
if (!bobinasAActualizar.Any())
|
||||
{
|
||||
return (false, "No se encontraron bobinas para el remito, planta y fecha especificados. Es posible que ya hayan sido modificados.");
|
||||
}
|
||||
|
||||
// 2. Iniciar una transacción para asegurar que todas las actualizaciones se completen o ninguna.
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
// 3. Iterar sobre cada bobina y actualizarla.
|
||||
foreach (var bobina in bobinasAActualizar)
|
||||
{
|
||||
// Modificamos solo la fecha del remito.
|
||||
bobina.FechaRemito = dto.NuevaFechaRemito.Date;
|
||||
|
||||
// Reutilizamos el método UpdateAsync que ya maneja la lógica de historial.
|
||||
// Le pasamos un mensaje específico para el historial.
|
||||
await _stockBobinaRepository.UpdateAsync(bobina, idUsuario, transaction, "Fecha Remito Corregida");
|
||||
}
|
||||
|
||||
// 4. Si todo salió bien, confirmar la transacción.
|
||||
transaction.Commit();
|
||||
_logger.LogInformation(
|
||||
"{Count} bobinas del remito {Remito} (Planta ID {IdPlanta}) actualizadas a nueva fecha {NuevaFecha} por Usuario ID {IdUsuario}.",
|
||||
bobinasAActualizar.Count(), dto.Remito, dto.IdPlanta, dto.NuevaFechaRemito.Date, idUsuario
|
||||
);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 5. Si algo falla, revertir todos los cambios.
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error transaccional al actualizar fecha de remito {Remito}.", dto.Remito);
|
||||
return (false, $"Error interno al actualizar el lote: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Exito, string? Error)> IngresarBobinaLoteAsync(CreateStockBobinaLoteDto loteDto, int idUsuario)
|
||||
{
|
||||
// --- FASE 1: VALIDACIÓN PREVIA (FUERA DE LA TRANSACCIÓN) ---
|
||||
|
||||
// Validación de la cabecera
|
||||
if (await _plantaRepository.GetByIdAsync(loteDto.IdPlanta) == null)
|
||||
return (false, "La planta especificada no es válida.");
|
||||
|
||||
// Validación de cada bobina del lote
|
||||
foreach (var bobinaDetalle in loteDto.Bobinas)
|
||||
{
|
||||
if (await _tipoBobinaRepository.GetByIdAsync(bobinaDetalle.IdTipoBobina) == null)
|
||||
{
|
||||
return (false, $"El tipo de bobina con ID {bobinaDetalle.IdTipoBobina} no es válido.");
|
||||
}
|
||||
|
||||
// Esta es la lectura que causaba el bloqueo. Ahora se hace ANTES de la transacción.
|
||||
if (await _stockBobinaRepository.GetByNroBobinaAsync(bobinaDetalle.NroBobina) != null)
|
||||
{
|
||||
return (false, $"El número de bobina '{bobinaDetalle.NroBobina}' ya existe en el sistema.");
|
||||
}
|
||||
}
|
||||
|
||||
// Validación de números de bobina duplicados dentro del mismo lote
|
||||
var nrosBobinaEnLote = loteDto.Bobinas.Select(b => b.NroBobina.Trim()).ToList();
|
||||
if (nrosBobinaEnLote.Count != nrosBobinaEnLote.Distinct().Count())
|
||||
{
|
||||
var duplicado = nrosBobinaEnLote.GroupBy(n => n).Where(g => g.Count() > 1).First().Key;
|
||||
return (false, $"El número de bobina '{duplicado}' está duplicado en el lote que intenta ingresar.");
|
||||
}
|
||||
|
||||
|
||||
// --- FASE 2: ESCRITURA TRANSACCIONAL ---
|
||||
|
||||
using var connection = _connectionFactory.CreateConnection();
|
||||
if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
// Ahora este bucle solo contiene operaciones de escritura. No habrá bloqueos.
|
||||
foreach (var bobinaDetalle in loteDto.Bobinas)
|
||||
{
|
||||
var nuevaBobina = new StockBobina
|
||||
{
|
||||
IdTipoBobina = bobinaDetalle.IdTipoBobina,
|
||||
NroBobina = bobinaDetalle.NroBobina,
|
||||
Peso = bobinaDetalle.Peso,
|
||||
IdPlanta = loteDto.IdPlanta,
|
||||
Remito = loteDto.Remito,
|
||||
FechaRemito = loteDto.FechaRemito.Date,
|
||||
IdEstadoBobina = 1, // 1 = Disponible
|
||||
FechaEstado = loteDto.FechaRemito.Date,
|
||||
IdPublicacion = null,
|
||||
IdSeccion = null,
|
||||
Obs = null
|
||||
};
|
||||
|
||||
var bobinaCreada = await _stockBobinaRepository.CreateAsync(nuevaBobina, idUsuario, transaction);
|
||||
if (bobinaCreada == null)
|
||||
{
|
||||
throw new DataException($"No se pudo crear el registro para la bobina '{nuevaBobina.NroBobina}'.");
|
||||
}
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Lote de {Count} bobinas para remito {Remito} ingresado por Usuario ID {UserId}.", loteDto.Bobinas.Count, loteDto.Remito, idUsuario);
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try { transaction.Rollback(); } catch { }
|
||||
_logger.LogError(ex, "Error al ingresar lote de bobinas para remito {Remito}", loteDto.Remito);
|
||||
return (false, $"Error interno al procesar el lote: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,16 +27,16 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
|
||||
// Reporte Distribucion Canillas (MC005) - Este es un reporte más complejo
|
||||
Task<(
|
||||
IEnumerable<DetalleDistribucionCanillaDto> Canillas,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc,
|
||||
IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq,
|
||||
IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos, // Para SP_ObtenerCtrlDevoluciones
|
||||
IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan, // Para SP_DistCanillasCantidadEntradaSalida
|
||||
IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias, // <--- NUEVO para SP_DistCanillasCantidadEntradaSalidaOtrosDias
|
||||
string? Error
|
||||
)> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa);
|
||||
IEnumerable<DetalleDistribucionCanillaDto> Canillas,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc,
|
||||
IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq,
|
||||
IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos,
|
||||
IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan,
|
||||
IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias,
|
||||
string? Error
|
||||
)> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa, bool? esAccionista);
|
||||
|
||||
// Reporte Tiradas por Publicación y Secciones (RR008)
|
||||
Task<(IEnumerable<TiradasPublicacionesSeccionesDto> Data, string? Error)> ObtenerTiradasPublicacionesSeccionesAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta, int idPlanta);
|
||||
|
||||
@@ -218,30 +218,66 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
}
|
||||
|
||||
public async Task<(
|
||||
IEnumerable<DetalleDistribucionCanillaDto> Canillas,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc,
|
||||
IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq,
|
||||
IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos,
|
||||
IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan,
|
||||
IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias,
|
||||
string? Error
|
||||
)> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa)
|
||||
IEnumerable<DetalleDistribucionCanillaDto> Canillas,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc,
|
||||
IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq,
|
||||
IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq,
|
||||
IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos,
|
||||
IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan,
|
||||
IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias,
|
||||
string? Error
|
||||
)> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa, bool? esAccionista)
|
||||
{
|
||||
try
|
||||
{
|
||||
var canillasTask = _reportesRepository.GetDetalleDistribucionCanillasPubliAsync(fecha, idEmpresa);
|
||||
var canillasAccTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubliAsync(fecha, idEmpresa);
|
||||
// Función helper para convertir fechas a UTC
|
||||
Func<IEnumerable<DetalleDistribucionCanillaDto>, IEnumerable<DetalleDistribucionCanillaDto>> toUtc =
|
||||
items => items?.Select(c => { if (c.Fecha.HasValue) c.Fecha = DateTime.SpecifyKind(c.Fecha.Value.Date, DateTimeKind.Utc); return c; }).ToList()
|
||||
?? Enumerable.Empty<DetalleDistribucionCanillaDto>();
|
||||
|
||||
// --- NUEVA LÓGICA PARA "TODAS LAS EMPRESAS" ---
|
||||
if (idEmpresa == 0)
|
||||
{
|
||||
Task<IEnumerable<DetalleDistribucionCanillaDto>> canillasTask = Task.FromResult(Enumerable.Empty<DetalleDistribucionCanillaDto>());
|
||||
Task<IEnumerable<DetalleDistribucionCanillaDto>> canillasAccTask = Task.FromResult(Enumerable.Empty<DetalleDistribucionCanillaDto>());
|
||||
|
||||
if (esAccionista == true) // Solo accionistas
|
||||
{
|
||||
canillasAccTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(fecha);
|
||||
}
|
||||
else // Solo canillitas (o si es null, por defecto canillitas)
|
||||
{
|
||||
canillasTask = _reportesRepository.GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(fecha);
|
||||
}
|
||||
|
||||
await Task.WhenAll(canillasTask, canillasAccTask);
|
||||
|
||||
return (
|
||||
toUtc(await canillasTask),
|
||||
toUtc(await canillasAccTask),
|
||||
Enumerable.Empty<DetalleDistribucionCanillaAllDto>(), // El resumen no aplica
|
||||
Enumerable.Empty<DetalleDistribucionCanillaDto>(), // Liquidaciones de otras fechas no aplican en esta vista simplificada
|
||||
Enumerable.Empty<DetalleDistribucionCanillaDto>(),
|
||||
Enumerable.Empty<ObtenerCtrlDevolucionesDto>(), // Control de devoluciones no aplica
|
||||
Enumerable.Empty<ControlDevolucionesReporteDto>(),
|
||||
Enumerable.Empty<DevueltosOtrosDiasDto>(),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
// --- LÓGICA ORIGINAL PARA UNA EMPRESA ESPECÍFICA ---
|
||||
var canillasTaskOriginal = _reportesRepository.GetDetalleDistribucionCanillasPubliAsync(fecha, idEmpresa);
|
||||
var canillasAccTaskOriginal = _reportesRepository.GetDetalleDistribucionCanillasAccPubliAsync(fecha, idEmpresa);
|
||||
var canillasAllTask = _reportesRepository.GetDetalleDistribucionCanillasAllPubliAsync(fecha, idEmpresa);
|
||||
var canillasFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasPubliFechaLiqAsync(fecha, idEmpresa);
|
||||
var canillasAccFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubliFechaLiqAsync(fecha, idEmpresa);
|
||||
var ctrlDevolucionesRemitosTask = _reportesRepository.GetReporteObtenerCtrlDevolucionesAsync(fecha, idEmpresa); // SP_ObtenerCtrlDevoluciones
|
||||
var ctrlDevolucionesParaDistCanTask = _reportesRepository.GetReporteCtrlDevolucionesParaDistCanAsync(fecha, idEmpresa); // SP_DistCanillasCantidadEntradaSalida
|
||||
var ctrlDevolucionesOtrosDiasTask = _reportesRepository.GetEntradaSalidaOtrosDiasAsync(fecha, idEmpresa); // SP_DistCanillasCantidadEntradaSalidaOtrosDias
|
||||
var ctrlDevolucionesRemitosTask = _reportesRepository.GetReporteObtenerCtrlDevolucionesAsync(fecha, idEmpresa);
|
||||
var ctrlDevolucionesParaDistCanTask = _reportesRepository.GetReporteCtrlDevolucionesParaDistCanAsync(fecha, idEmpresa);
|
||||
var ctrlDevolucionesOtrosDiasTask = _reportesRepository.GetEntradaSalidaOtrosDiasAsync(fecha, idEmpresa);
|
||||
|
||||
await Task.WhenAll(
|
||||
canillasTask, canillasAccTask, canillasAllTask,
|
||||
canillasTaskOriginal, canillasAccTaskOriginal, canillasAllTask,
|
||||
canillasFechaLiqTask, canillasAccFechaLiqTask,
|
||||
ctrlDevolucionesRemitosTask, ctrlDevolucionesParaDistCanTask,
|
||||
ctrlDevolucionesOtrosDiasTask
|
||||
@@ -250,13 +286,9 @@ namespace GestionIntegral.Api.Services.Reportes
|
||||
var detallesOriginales = await ctrlDevolucionesParaDistCanTask ?? Enumerable.Empty<ControlDevolucionesReporteDto>();
|
||||
var detallesOrdenados = detallesOriginales.OrderBy(d => d.Tipo).ToList();
|
||||
|
||||
Func<IEnumerable<DetalleDistribucionCanillaDto>, IEnumerable<DetalleDistribucionCanillaDto>> toUtc =
|
||||
items => items?.Select(c => { if (c.Fecha.HasValue) c.Fecha = DateTime.SpecifyKind(c.Fecha.Value.Date, DateTimeKind.Utc); return c; }).ToList()
|
||||
?? Enumerable.Empty<DetalleDistribucionCanillaDto>();
|
||||
|
||||
return (
|
||||
toUtc(await canillasTask),
|
||||
toUtc(await canillasAccTask),
|
||||
toUtc(await canillasTaskOriginal),
|
||||
toUtc(await canillasAccTaskOriginal),
|
||||
await canillasAllTask ?? Enumerable.Empty<DetalleDistribucionCanillaAllDto>(),
|
||||
toUtc(await canillasFechaLiqTask),
|
||||
toUtc(await canillasAccFechaLiqTask),
|
||||
|
||||
@@ -17,8 +17,8 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<DebitoAutomaticoService> _logger;
|
||||
|
||||
private const string NRO_PRESTACION = "123456";
|
||||
private const string ORIGEN_EMPRESA = "ELDIA";
|
||||
private const string NRO_PRESTACION = "26435"; // Reemplazar por el número real
|
||||
private const string ORIGEN_EMPRESA = "EMPRESA";
|
||||
|
||||
public DebitoAutomaticoService(
|
||||
IFacturaRepository facturaRepository,
|
||||
@@ -40,9 +40,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
|
||||
public async Task<(string? ContenidoArchivo, string? NombreArchivo, string? Error)> GenerarArchivoPagoDirecto(int anio, int mes, int idUsuario)
|
||||
{
|
||||
// Se define la identificación del archivo.
|
||||
// Este número debe ser gestionado para no repetirse en archivos generados
|
||||
// para la misma prestación y fecha.
|
||||
// Este número debe ser gestionado para no repetirse. Por ahora, lo mantenemos como 1.
|
||||
const int identificacionArchivo = 1;
|
||||
|
||||
var periodo = $"{anio}-{mes:D2}";
|
||||
@@ -62,8 +60,6 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
|
||||
var importeTotal = facturasParaDebito.Sum(f => f.Factura.ImporteFinal);
|
||||
var cantidadRegistros = facturasParaDebito.Count();
|
||||
|
||||
// Se utiliza la variable 'identificacionArchivo' para nombrar el archivo.
|
||||
var nombreArchivo = $"{NRO_PRESTACION}{fechaGeneracion:yyyyMMdd}{identificacionArchivo}.txt";
|
||||
|
||||
var nuevoLote = new LoteDebito
|
||||
@@ -78,13 +74,11 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
if (loteCreado == null) throw new DataException("No se pudo crear el registro del lote de débito.");
|
||||
|
||||
var sb = new StringBuilder();
|
||||
// Se pasa la 'identificacionArchivo' al método que crea el Header.
|
||||
sb.Append(CrearRegistroHeader(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
|
||||
foreach (var item in facturasParaDebito)
|
||||
{
|
||||
sb.Append(CrearRegistroDetalle(item.Factura, item.Suscriptor));
|
||||
}
|
||||
// Se pasa la 'identificacionArchivo' al método que crea el Trailer.
|
||||
sb.Append(CrearRegistroTrailer(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
|
||||
|
||||
var idsFacturas = facturasParaDebito.Select(f => f.Factura.IdFactura);
|
||||
@@ -108,17 +102,18 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
var facturas = await _facturaRepository.GetByPeriodoAsync(periodo);
|
||||
var resultado = new List<(Factura, Suscriptor)>();
|
||||
|
||||
foreach (var f in facturas.Where(fa => fa.EstadoPago == "Pendiente"))
|
||||
// Filtramos por estado Y POR TIPO DE FACTURA
|
||||
foreach (var f in facturas.Where(fa =>
|
||||
(fa.EstadoPago == "Pendiente" || fa.EstadoPago == "Pagada Parcialmente" || fa.EstadoPago == "Rechazada") &&
|
||||
fa.TipoFactura == "Mensual"
|
||||
))
|
||||
{
|
||||
var suscriptor = await _suscriptorRepository.GetByIdAsync(f.IdSuscriptor);
|
||||
|
||||
// Se valida que el CBU de Banelco (22 caracteres) exista antes de intentar la conversión.
|
||||
if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU) || suscriptor.CBU.Length != 22)
|
||||
{
|
||||
_logger.LogWarning("Suscriptor ID {IdSuscriptor} omitido del lote de débito por CBU inválido o ausente (se esperan 22 dígitos).", suscriptor?.IdSuscriptor);
|
||||
_logger.LogWarning("Suscriptor ID {IdSuscriptor} omitido del lote de débito por CBU inválido o ausente (se esperan 22 dígitos).", f.IdSuscriptor);
|
||||
continue;
|
||||
}
|
||||
|
||||
var formaPago = await _formaPagoRepository.GetByIdAsync(suscriptor.IdFormaPagoPreferida);
|
||||
if (formaPago != null && formaPago.RequiereCBU)
|
||||
{
|
||||
@@ -128,26 +123,13 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
return resultado;
|
||||
}
|
||||
|
||||
// Lógica de conversión de CBU.
|
||||
private string ConvertirCbuBanelcoASnp(string cbu22)
|
||||
{
|
||||
if (string.IsNullOrEmpty(cbu22) || cbu22.Length != 22)
|
||||
{
|
||||
_logger.LogError("Se intentó convertir un CBU inválido de {Length} caracteres. Se devolverá un campo vacío.", cbu22?.Length ?? 0);
|
||||
// Devolver un string de 26 espacios/ceros según la preferencia del banco para campos erróneos.
|
||||
return "".PadRight(26);
|
||||
}
|
||||
|
||||
// El formato SNP de 26 se obtiene insertando un "0" al inicio y "000" después del 8vo caracter del CBU de 22.
|
||||
// Formato Banelco (22): [BBBSSSSX] [T....Y]
|
||||
// Posiciones: (0-7) (8-21)
|
||||
// Formato SNP (26): 0[BBBSSSSX]000[T....Y]
|
||||
if (string.IsNullOrEmpty(cbu22) || cbu22.Length != 22) return "".PadRight(26);
|
||||
try
|
||||
{
|
||||
string bloque1 = cbu22.Substring(0, 8); // Contiene código de banco, sucursal y DV del bloque 1.
|
||||
string bloque2 = cbu22.Substring(8); // Contiene el resto de la cadena.
|
||||
|
||||
// Reconstruir en formato SNP de 26 dígitos según el instructivo.
|
||||
string bloque1 = cbu22.Substring(0, 8);
|
||||
string bloque2 = cbu22.Substring(8);
|
||||
return $"0{bloque1}000{bloque2}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -157,9 +139,10 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
}
|
||||
}
|
||||
|
||||
// --- Métodos de Formateo y Mapeo ---
|
||||
// --- Helpers de Formateo ---
|
||||
private string FormatString(string? value, int length) => (value ?? "").PadRight(length);
|
||||
private string FormatNumeric(long value, int length) => value.ToString().PadLeft(length, '0');
|
||||
private string FormatNumericString(string? value, int length) => (value ?? "").PadLeft(length, '0');
|
||||
private string MapTipoDocumento(string tipo) => tipo.ToUpper() switch
|
||||
{
|
||||
"DNI" => "0096",
|
||||
@@ -167,17 +150,17 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
"CUIL" => "0086",
|
||||
"LE" => "0089",
|
||||
"LC" => "0090",
|
||||
_ => "0000" // Tipo no especificado o C.I. Policía Federal según anexo.
|
||||
_ => "0000"
|
||||
};
|
||||
|
||||
private string CrearRegistroHeader(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("00"); // Tipo de Registro Header
|
||||
sb.Append(FormatString(NRO_PRESTACION, 6));
|
||||
sb.Append("C"); // Servicio: Sistema Nacional de Pagos
|
||||
sb.Append("00");
|
||||
sb.Append(FormatNumericString(NRO_PRESTACION, 6));
|
||||
sb.Append("C");
|
||||
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
||||
sb.Append(FormatString(identificacionArchivo.ToString(), 1)); // Identificación de Archivo
|
||||
sb.Append(FormatString(identificacionArchivo.ToString(), 1));
|
||||
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
||||
sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
|
||||
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
||||
@@ -188,35 +171,33 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
|
||||
private string CrearRegistroDetalle(Factura factura, Suscriptor suscriptor)
|
||||
{
|
||||
// Convertimos el CBU de 22 (Banelco) a 26 (SNP) antes de usarlo.
|
||||
string cbu26 = ConvertirCbuBanelcoASnp(suscriptor.CBU!);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("0370"); // Tipo de Registro Detalle (Orden de Débito)
|
||||
sb.Append(FormatString(suscriptor.IdSuscriptor.ToString(), 22)); // Identificación de Cliente
|
||||
sb.Append(FormatString(cbu26, 26)); // CBU en formato SNP de 26 caracteres.
|
||||
sb.Append(FormatString($"SUSC-{factura.IdFactura}", 15)); // Referencia Unívoca de la factura.
|
||||
sb.Append("0370");
|
||||
sb.Append(FormatString(suscriptor.IdSuscriptor.ToString(), 22));
|
||||
sb.Append(cbu26);
|
||||
sb.Append(FormatString($"SUSC-{factura.IdFactura}", 15));
|
||||
sb.Append(factura.FechaVencimiento.ToString("yyyyMMdd"));
|
||||
sb.Append(FormatNumeric((long)(factura.ImporteFinal * 100), 14));
|
||||
sb.Append(FormatNumeric(0, 8)); // Fecha 2do Vencimiento
|
||||
sb.Append(FormatNumeric(0, 14)); // Importe 2do Vencimiento
|
||||
sb.Append(FormatNumeric(0, 8)); // Fecha 3er Vencimiento
|
||||
sb.Append(FormatNumeric(0, 14)); // Importe 3er Vencimiento
|
||||
sb.Append("0"); // Moneda (0 = Pesos)
|
||||
sb.Append(FormatString("", 3)); // Motivo Rechazo (vacío en el envío)
|
||||
sb.Append("0");
|
||||
sb.Append(FormatString("", 3));
|
||||
sb.Append(FormatString(MapTipoDocumento(suscriptor.TipoDocumento), 4));
|
||||
sb.Append(FormatString(suscriptor.NroDocumento, 11));
|
||||
sb.Append(FormatString("", 22)); // Nueva ID Cliente
|
||||
sb.Append(FormatString("", 26)); // Nueva CBU
|
||||
sb.Append(FormatNumeric(0, 14)); // Importe Mínimo
|
||||
sb.Append(FormatNumeric(0, 8)); // Fecha Próximo Vencimiento
|
||||
sb.Append(FormatString("", 22)); // Identificación Cuenta Anterior
|
||||
sb.Append(FormatString("", 40)); // Mensaje ATM
|
||||
sb.Append(FormatString($"Susc.{factura.Periodo}", 10)); // Concepto Factura
|
||||
sb.Append(FormatNumeric(0, 8)); // Fecha de Cobro
|
||||
sb.Append(FormatNumeric(0, 14)); // Importe Cobrado
|
||||
sb.Append(FormatNumeric(0, 8)); // Fecha de Acreditamiento
|
||||
sb.Append(FormatString("", 26)); // Libre
|
||||
sb.Append(FormatNumericString(suscriptor.NroDocumento, 11));
|
||||
sb.Append(FormatString("", 22));
|
||||
sb.Append(FormatString("", 26));
|
||||
sb.Append(FormatNumeric(0, 14));
|
||||
sb.Append(FormatNumeric(0, 8));
|
||||
sb.Append(FormatString("", 22));
|
||||
sb.Append(FormatString("", 40));
|
||||
sb.Append(FormatString($"Susc.{factura.Periodo}", 10));
|
||||
sb.Append(FormatNumeric(0, 8));
|
||||
sb.Append(FormatNumeric(0, 14));
|
||||
sb.Append(FormatNumeric(0, 8));
|
||||
sb.Append(FormatString("", 26));
|
||||
sb.Append("\r\n");
|
||||
return sb.ToString();
|
||||
}
|
||||
@@ -224,16 +205,16 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
private string CrearRegistroTrailer(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("99"); // Tipo de Registro Trailer
|
||||
sb.Append(FormatString(NRO_PRESTACION, 6));
|
||||
sb.Append("C"); // Servicio: Sistema Nacional de Pagos
|
||||
sb.Append("99");
|
||||
sb.Append(FormatNumericString(NRO_PRESTACION, 6));
|
||||
sb.Append("C");
|
||||
sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
|
||||
sb.Append(FormatString(identificacionArchivo.ToString(), 1)); // Identificación de Archivo
|
||||
sb.Append(FormatString(identificacionArchivo.ToString(), 1));
|
||||
sb.Append(FormatString(ORIGEN_EMPRESA, 7));
|
||||
sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
|
||||
sb.Append(FormatNumeric(cantidadRegistros, 7));
|
||||
sb.Append(FormatString("", 304));
|
||||
// La última línea del archivo no lleva salto de línea (\r\n).
|
||||
sb.Append("\r\n");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -171,10 +171,13 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
DescuentoAplicado = descuentoPromocionesTotal,
|
||||
ImporteFinal = importeFinal,
|
||||
EstadoPago = "Pendiente",
|
||||
EstadoFacturacion = "Pendiente de Facturar"
|
||||
EstadoFacturacion = "Pendiente de Facturar",
|
||||
TipoFactura = "Mensual"
|
||||
};
|
||||
|
||||
var facturaCreada = await _facturaRepository.CreateAsync(nuevaFactura, transaction);
|
||||
if (facturaCreada == null) throw new DataException($"No se pudo crear la factura para suscriptor ID {idSuscriptor} y empresa ID {idEmpresa}");
|
||||
|
||||
facturasCreadas.Add(facturaCreada);
|
||||
foreach (var detalle in detallesParaFactura)
|
||||
{
|
||||
@@ -278,11 +281,12 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion)
|
||||
public async Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(
|
||||
int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura)
|
||||
{
|
||||
var periodo = $"{anio}-{mes:D2}";
|
||||
var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo, nombreSuscriptor, estadoPago, estadoFacturacion);
|
||||
var detallesData = await _facturaDetalleRepository.GetDetallesPorPeriodoAsync(periodo); // Necesitaremos este nuevo método en el repo
|
||||
var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo, nombreSuscriptor, estadoPago, estadoFacturacion, tipoFactura);
|
||||
var detallesData = await _facturaDetalleRepository.GetDetallesPorPeriodoAsync(periodo);
|
||||
var empresas = await _empresaRepository.GetAllAsync(null, null);
|
||||
|
||||
var resumenes = facturasData
|
||||
@@ -301,10 +305,18 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
EstadoPago = itemFactura.Factura.EstadoPago,
|
||||
EstadoFacturacion = itemFactura.Factura.EstadoFacturacion,
|
||||
NumeroFactura = itemFactura.Factura.NumeroFactura,
|
||||
TotalPagado = itemFactura.TotalPagado,
|
||||
|
||||
// Faltaba esta línea para pasar el tipo de factura al frontend.
|
||||
TipoFactura = itemFactura.Factura.TipoFactura,
|
||||
|
||||
Detalles = detallesData
|
||||
.Where(d => d.IdFactura == itemFactura.Factura.IdFactura)
|
||||
.Select(d => new FacturaDetalleDto { Descripcion = d.Descripcion, ImporteNeto = d.ImporteNeto })
|
||||
.ToList()
|
||||
.ToList(),
|
||||
|
||||
// Pasamos el id del suscriptor para facilitar las cosas en el frontend
|
||||
IdSuscriptor = itemFactura.Factura.IdSuscriptor
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
@@ -314,7 +326,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
NombreSuscriptor = primerItem.NombreSuscriptor,
|
||||
Facturas = facturasConsolidadas,
|
||||
ImporteTotal = facturasConsolidadas.Sum(f => f.ImporteFinal),
|
||||
SaldoPendienteTotal = facturasConsolidadas.Sum(f => f.EstadoPago == "Pagada" ? 0 : f.ImporteFinal)
|
||||
SaldoPendienteTotal = facturasConsolidadas.Sum(f => f.ImporteFinal - f.TotalPagado)
|
||||
};
|
||||
});
|
||||
|
||||
@@ -578,7 +590,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction)
|
||||
public async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction)
|
||||
{
|
||||
decimal importeTotal = 0;
|
||||
var diasDeEntrega = suscripcion.DiasEntrega.Split(',').ToHashSet();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Data;
|
||||
using GestionIntegral.Api.Dtos.Comunicaciones;
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
using GestionIntegral.Api.Models.Suscripciones;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
@@ -7,8 +9,10 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
Task<(bool Exito, string? Mensaje, LoteDeEnvioResumenDto? ResultadoEnvio)> GenerarFacturacionMensual(int anio, int mes, int idUsuario);
|
||||
Task<IEnumerable<LoteDeEnvioHistorialDto>> ObtenerHistorialLotesEnvio(int? anio, int? mes);
|
||||
Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion);
|
||||
Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(
|
||||
int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura);
|
||||
Task<(bool Exito, string? Error, string? EmailDestino)> EnviarFacturaPdfPorEmail(int idFactura, int idUsuario);
|
||||
Task<(bool Exito, string? Error)> ActualizarNumeroFactura(int idFactura, string numeroFactura, int idUsuario);
|
||||
Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction);
|
||||
}
|
||||
}
|
||||
@@ -70,14 +70,11 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
var factura = await _facturaRepository.GetByIdAsync(createDto.IdFactura);
|
||||
if (factura == null) return (null, "La factura especificada no existe.");
|
||||
|
||||
// Usar EstadoPago para la validación
|
||||
if (factura.EstadoPago == "Anulada") return (null, "No se puede registrar un pago sobre una factura anulada.");
|
||||
|
||||
var formaPago = await _formaPagoRepository.GetByIdAsync(createDto.IdFormaPago);
|
||||
if (formaPago == null || !formaPago.Activo) return (null, "La forma de pago no es válida.");
|
||||
|
||||
// Obtenemos la suma de pagos ANTERIORES
|
||||
var totalPagadoAnteriormente = await _pagoRepository.GetTotalPagadoAprobadoAsync(createDto.IdFactura, transaction);
|
||||
|
||||
var nuevoPago = new Pago
|
||||
@@ -96,37 +93,31 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
var pagoCreado = await _pagoRepository.CreateAsync(nuevoPago, transaction);
|
||||
if (pagoCreado == null) throw new DataException("No se pudo registrar el pago.");
|
||||
|
||||
// Calculamos el nuevo total EN MEMORIA
|
||||
var nuevoTotalPagado = totalPagadoAnteriormente + pagoCreado.Monto;
|
||||
|
||||
// Comparamos y actualizamos el estado si es necesario
|
||||
// CORRECCIÓN: Usar EstadoPago y el método correcto del repositorio
|
||||
if (factura.EstadoPago != "Pagada" && nuevoTotalPagado >= factura.ImporteFinal)
|
||||
// Nueva lógica para manejar todos los estados de pago
|
||||
string nuevoEstadoPago = factura.EstadoPago;
|
||||
if (nuevoTotalPagado >= factura.ImporteFinal)
|
||||
{
|
||||
bool actualizado = await _facturaRepository.UpdateEstadoPagoAsync(factura.IdFactura, "Pagada", transaction);
|
||||
if (!actualizado) throw new DataException("No se pudo actualizar el estado de la factura a 'Pagada'.");
|
||||
nuevoEstadoPago = "Pagada";
|
||||
}
|
||||
else if (nuevoTotalPagado > 0)
|
||||
{
|
||||
nuevoEstadoPago = "Pagada Parcialmente";
|
||||
}
|
||||
// Si nuevoTotalPagado es 0, el estado no cambia.
|
||||
|
||||
// Solo actualizamos si el estado calculado es diferente al actual.
|
||||
if (nuevoEstadoPago != factura.EstadoPago)
|
||||
{
|
||||
bool actualizado = await _facturaRepository.UpdateEstadoPagoAsync(factura.IdFactura, nuevoEstadoPago, transaction);
|
||||
if (!actualizado) throw new DataException($"No se pudo actualizar el estado de la factura a '{nuevoEstadoPago}'.");
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Pago manual ID {IdPago} registrado para Factura ID {IdFactura} por Usuario ID {IdUsuario}", pagoCreado.IdPago, pagoCreado.IdFactura, idUsuario);
|
||||
|
||||
// Construimos el DTO de respuesta SIN volver a consultar la base de datos
|
||||
var usuario = await _usuarioRepository.GetByIdAsync(idUsuario);
|
||||
var dto = new PagoDto
|
||||
{
|
||||
IdPago = pagoCreado.IdPago,
|
||||
IdFactura = pagoCreado.IdFactura,
|
||||
FechaPago = pagoCreado.FechaPago.ToString("yyyy-MM-dd"),
|
||||
IdFormaPago = pagoCreado.IdFormaPago,
|
||||
NombreFormaPago = formaPago.Nombre,
|
||||
Monto = pagoCreado.Monto,
|
||||
Estado = pagoCreado.Estado,
|
||||
Referencia = pagoCreado.Referencia,
|
||||
Observaciones = pagoCreado.Observaciones,
|
||||
IdUsuarioRegistro = pagoCreado.IdUsuarioRegistro,
|
||||
NombreUsuarioRegistro = usuario != null ? $"{usuario.Nombre} {usuario.Apellido}" : "N/A"
|
||||
};
|
||||
|
||||
var dto = await MapToDto(pagoCreado); // MapToDto ahora es más simple
|
||||
return (dto, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -3,12 +3,8 @@ using GestionIntegral.Api.Data.Repositories.Distribucion;
|
||||
using GestionIntegral.Api.Data.Repositories.Suscripciones;
|
||||
using GestionIntegral.Api.Dtos.Suscripciones;
|
||||
using GestionIntegral.Api.Models.Suscripciones;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Globalization;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Suscripciones
|
||||
{
|
||||
@@ -18,23 +14,32 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
private readonly ISuscriptorRepository _suscriptorRepository;
|
||||
private readonly IPublicacionRepository _publicacionRepository;
|
||||
private readonly IPromocionRepository _promocionRepository;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
private readonly IFacturaRepository _facturaRepository;
|
||||
private readonly IFacturaDetalleRepository _facturaDetalleRepository;
|
||||
private readonly IFacturacionService _facturacionService;
|
||||
private readonly ILogger<SuscripcionService> _logger;
|
||||
private readonly DbConnectionFactory _connectionFactory;
|
||||
|
||||
public SuscripcionService(
|
||||
ISuscripcionRepository suscripcionRepository,
|
||||
ISuscriptorRepository suscriptorRepository,
|
||||
IPublicacionRepository publicacionRepository,
|
||||
IPromocionRepository promocionRepository,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ILogger<SuscripcionService> logger)
|
||||
ISuscriptorRepository suscriptorRepository,
|
||||
IPublicacionRepository publicacionRepository,
|
||||
IPromocionRepository promocionRepository,
|
||||
IFacturaRepository facturaRepository,
|
||||
IFacturaDetalleRepository facturaDetalleRepository,
|
||||
IFacturacionService facturacionService,
|
||||
ILogger<SuscripcionService> logger,
|
||||
DbConnectionFactory connectionFactory)
|
||||
{
|
||||
_suscripcionRepository = suscripcionRepository;
|
||||
_suscriptorRepository = suscriptorRepository;
|
||||
_publicacionRepository = publicacionRepository;
|
||||
_promocionRepository = promocionRepository;
|
||||
_connectionFactory = connectionFactory;
|
||||
_facturaRepository = facturaRepository;
|
||||
_facturaDetalleRepository = facturaDetalleRepository;
|
||||
_facturacionService = facturacionService;
|
||||
_logger = logger;
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
private PromocionDto MapPromocionToDto(Promocion promo) => new PromocionDto
|
||||
@@ -122,6 +127,53 @@ namespace GestionIntegral.Api.Services.Suscripciones
|
||||
var creada = await _suscripcionRepository.CreateAsync(nuevaSuscripcion, transaction);
|
||||
if (creada == null) throw new DataException("Error al crear la suscripción.");
|
||||
|
||||
var ultimoPeriodoFacturadoStr = await _facturaRepository.GetUltimoPeriodoFacturadoAsync();
|
||||
if (ultimoPeriodoFacturadoStr != null)
|
||||
{
|
||||
var ultimoPeriodo = DateTime.ParseExact(ultimoPeriodoFacturadoStr, "yyyy-MM", CultureInfo.InvariantCulture);
|
||||
var periodoSuscripcion = new DateTime(creada.FechaInicio.Year, creada.FechaInicio.Month, 1);
|
||||
|
||||
if (periodoSuscripcion <= ultimoPeriodo)
|
||||
{
|
||||
_logger.LogInformation("Suscripción en período ya cerrado detectada. Generando factura de alta pro-rata.");
|
||||
|
||||
decimal importeProporcional = await _facturacionService.CalcularImporteParaSuscripcion(creada, creada.FechaInicio.Year, creada.FechaInicio.Month, transaction);
|
||||
|
||||
if (importeProporcional > 0)
|
||||
{
|
||||
var facturaDeAlta = new Factura
|
||||
{
|
||||
IdSuscriptor = creada.IdSuscriptor,
|
||||
Periodo = creada.FechaInicio.ToString("yyyy-MM"),
|
||||
FechaEmision = DateTime.Now.Date,
|
||||
FechaVencimiento = DateTime.Now.AddDays(10).Date,
|
||||
ImporteBruto = importeProporcional,
|
||||
ImporteFinal = importeProporcional,
|
||||
EstadoPago = "Pendiente",
|
||||
EstadoFacturacion = "Pendiente de Facturar",
|
||||
TipoFactura = "Alta"
|
||||
};
|
||||
|
||||
var facturaCreada = await _facturaRepository.CreateAsync(facturaDeAlta, transaction);
|
||||
if (facturaCreada == null) throw new DataException("No se pudo crear la factura de alta.");
|
||||
|
||||
var publicacion = await _publicacionRepository.GetByIdSimpleAsync(creada.IdPublicacion);
|
||||
var finDeMes = new DateTime(creada.FechaInicio.Year, creada.FechaInicio.Month, 1).AddMonths(1).AddDays(-1);
|
||||
|
||||
await _facturaDetalleRepository.CreateAsync(new FacturaDetalle
|
||||
{
|
||||
IdFactura = facturaCreada.IdFactura,
|
||||
IdSuscripcion = creada.IdSuscripcion,
|
||||
Descripcion = $"Suscripción proporcional {publicacion?.Nombre} ({creada.FechaInicio:dd/MM} al {finDeMes:dd/MM})",
|
||||
ImporteBruto = importeProporcional,
|
||||
ImporteNeto = importeProporcional,
|
||||
DescuentoAplicado = 0
|
||||
}, transaction);
|
||||
|
||||
_logger.LogInformation("Factura de alta #{IdFactura} por ${Importe} generada para la nueva suscripción #{IdSuscripcion}.", facturaCreada.IdFactura, importeProporcional, creada.IdSuscripcion);
|
||||
}
|
||||
}
|
||||
}
|
||||
transaction.Commit();
|
||||
_logger.LogInformation("Suscripción ID {Id} creada por Usuario ID {UserId}.", creada.IdSuscripcion, idUsuario);
|
||||
return (await MapToDto(creada), null);
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"MailSettings": {
|
||||
"SmtpHost": "",
|
||||
"SmtpPort": 0,
|
||||
"SenderName": "",
|
||||
"SenderEmail": "",
|
||||
"SmtpUser": "",
|
||||
"SmtpPass": ""
|
||||
"SmtpHost": "192.168.5.201",
|
||||
"SmtpPort": 587,
|
||||
"SenderName": "Club - Diario El Día",
|
||||
"SenderEmail": "alertas@eldia.com",
|
||||
"SmtpUser": "alertas@eldia.com",
|
||||
"SmtpPass": "@Alertas713550@"
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
|
||||
const isEditing = Boolean(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
// Esta función se encarga de cargar los datos de los dropdowns.
|
||||
const fetchDropdownData = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
@@ -133,7 +134,9 @@ const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
|
||||
idTipoPago: Number(idTipoPago),
|
||||
detalle: detalle || undefined,
|
||||
};
|
||||
// << INICIO DE LA CORRECCIÓN >>
|
||||
await onSubmit(dataToSubmit, initialData.idPago);
|
||||
// << FIN DE LA CORRECCIÓN >>
|
||||
} else {
|
||||
const dataToSubmit: CreatePagoDistribuidorDto = {
|
||||
idDistribuidor: Number(idDistribuidor),
|
||||
@@ -147,7 +150,9 @@ const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
|
||||
};
|
||||
await onSubmit(dataToSubmit);
|
||||
}
|
||||
|
||||
onClose();
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de PagoDistribuidorFormModal:", error);
|
||||
} finally {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
||||
import type { CambiarEstadoBobinaDto } from '../../../models/dtos/Impresion/CambiarEstadoBobinaDto';
|
||||
import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto';
|
||||
// --- CAMBIO: Importar PublicacionDropdownDto ---
|
||||
import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto';
|
||||
import estadoBobinaService from '../../../services/Impresion/estadoBobinaService';
|
||||
@@ -33,272 +32,288 @@ const ID_ESTADO_EN_USO = 2; // Usaremos este consistentemente
|
||||
const ID_ESTADO_DANADA = 3;
|
||||
|
||||
interface StockBobinaCambioEstadoModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise<void>;
|
||||
bobinaActual: StockBobinaDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise<void>;
|
||||
bobinaActual: StockBobinaDto | null;
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
bobinaActual,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
bobinaActual,
|
||||
errorMessage,
|
||||
clearErrorMessage
|
||||
}) => {
|
||||
const [nuevoEstadoId, setNuevoEstadoId] = useState<number | string>('');
|
||||
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
|
||||
const [idSeccion, setIdSeccion] = useState<number | string>('');
|
||||
const [obs, setObs] = useState('');
|
||||
const [fechaCambioEstado, setFechaCambioEstado] = useState('');
|
||||
const [nuevoEstadoId, setNuevoEstadoId] = useState<number | string>('');
|
||||
const [idPublicacion, setIdPublicacion] = useState<number | string>('');
|
||||
const [idSeccion, setIdSeccion] = useState<number | string>('');
|
||||
const [obs, setObs] = useState('');
|
||||
const [fechaCambioEstado, setFechaCambioEstado] = useState('');
|
||||
|
||||
const [estadosDisponibles, setEstadosDisponibles] = useState<EstadoBobinaDto[]>([]);
|
||||
// --- CAMBIO: Usar PublicacionDropdownDto para el estado ---
|
||||
const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]);
|
||||
const [estadosDisponibles, setEstadosDisponibles] = useState<EstadoBobinaDto[]>([]);
|
||||
const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDropdownData = async () => {
|
||||
if (!bobinaActual) return;
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const todosLosEstados = await estadoBobinaService.getAllEstadosBobina();
|
||||
let estadosFiltrados: EstadoBobinaDto[];
|
||||
|
||||
if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DISPONIBLE);
|
||||
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA);
|
||||
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_DISPONIBLE) {
|
||||
// --- CAMBIO: Usar ID_ESTADO_EN_USO ---
|
||||
estadosFiltrados = todosLosEstados.filter(
|
||||
e => e.idEstadoBobina === ID_ESTADO_EN_USO || e.idEstadoBobina === ID_ESTADO_DANADA
|
||||
);
|
||||
} else {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina);
|
||||
}
|
||||
|
||||
setEstadosDisponibles(estadosFiltrados);
|
||||
|
||||
const sePuedePonerEnUso = estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO);
|
||||
|
||||
if (sePuedePonerEnUso) {
|
||||
// --- CAMBIO: La data es PublicacionDropdownDto[] ---
|
||||
const publicacionesData: PublicacionDropdownDto[] = await publicacionService.getPublicacionesForDropdown(true);
|
||||
setPublicacionesDisponibles(publicacionesData);
|
||||
} else {
|
||||
setPublicacionesDisponibles([]);
|
||||
setIdPublicacion('');
|
||||
setIdSeccion('');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error al cargar datos para dropdowns (Cambio Estado Bobina)", error);
|
||||
setLocalErrors(prev => ({...prev, dropdowns: 'Error al cargar datos necesarios.'}));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open && bobinaActual) {
|
||||
fetchDropdownData();
|
||||
setNuevoEstadoId('');
|
||||
// Pre-cargar basado en si la bobina actual está "En Uso"
|
||||
if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
||||
setIdPublicacion(bobinaActual.idPublicacion?.toString() || '');
|
||||
// Solo pre-cargar sección si la publicación también estaba pre-cargada
|
||||
if (bobinaActual.idPublicacion) {
|
||||
setIdSeccion(bobinaActual.idSeccion?.toString() || '');
|
||||
} else {
|
||||
setIdSeccion('');
|
||||
}
|
||||
} else {
|
||||
setIdPublicacion('');
|
||||
setIdSeccion('');
|
||||
}
|
||||
setObs(bobinaActual.obs || '');
|
||||
setFechaCambioEstado(new Date().toISOString().split('T')[0]);
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, bobinaActual, clearErrorMessage]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSecciones = async () => {
|
||||
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO && idPublicacion) {
|
||||
useEffect(() => {
|
||||
const fetchDropdownData = async () => {
|
||||
if (!bobinaActual) return;
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true);
|
||||
setSeccionesDisponibles(data);
|
||||
const todosLosEstados = await estadoBobinaService.getAllEstadosBobina();
|
||||
let estadosFiltrados: EstadoBobinaDto[];
|
||||
|
||||
if (bobinaActual.idEstadoBobina === ID_ESTADO_DANADA) {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DISPONIBLE);
|
||||
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina === ID_ESTADO_DANADA);
|
||||
} else if (bobinaActual.idEstadoBobina === ID_ESTADO_DISPONIBLE) {
|
||||
estadosFiltrados = todosLosEstados.filter(
|
||||
e => e.idEstadoBobina === ID_ESTADO_EN_USO || e.idEstadoBobina === ID_ESTADO_DANADA
|
||||
);
|
||||
} else {
|
||||
estadosFiltrados = todosLosEstados.filter(e => e.idEstadoBobina !== bobinaActual.idEstadoBobina);
|
||||
}
|
||||
|
||||
setEstadosDisponibles(estadosFiltrados);
|
||||
|
||||
const sePuedePonerEnUso = estadosFiltrados.some(e => e.idEstadoBobina === ID_ESTADO_EN_USO);
|
||||
|
||||
if (sePuedePonerEnUso) {
|
||||
const publicacionesData: PublicacionDropdownDto[] = await publicacionService.getPublicacionesForDropdown(true);
|
||||
setPublicacionesDisponibles(publicacionesData);
|
||||
} else {
|
||||
setPublicacionesDisponibles([]);
|
||||
setIdPublicacion('');
|
||||
setIdSeccion('');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error al cargar secciones:", error);
|
||||
setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.'}));
|
||||
setSeccionesDisponibles([]);
|
||||
console.error("Error al cargar datos para dropdowns (Cambio Estado Bobina)", error);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar datos necesarios.' }));
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open && bobinaActual) {
|
||||
fetchDropdownData();
|
||||
setNuevoEstadoId('');
|
||||
// Pre-cargar basado en si la bobina actual está "En Uso"
|
||||
if (bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) {
|
||||
setIdPublicacion(bobinaActual.idPublicacion?.toString() || '');
|
||||
// Solo pre-cargar sección si la publicación también estaba pre-cargada
|
||||
if (bobinaActual.idPublicacion) {
|
||||
setIdSeccion(bobinaActual.idSeccion?.toString() || '');
|
||||
} else {
|
||||
setIdSeccion('');
|
||||
}
|
||||
} else {
|
||||
setIdPublicacion('');
|
||||
setIdSeccion('');
|
||||
}
|
||||
setObs(bobinaActual.obs || '');
|
||||
setFechaCambioEstado(new Date().toISOString().split('T')[0]);
|
||||
setLocalErrors({});
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, bobinaActual, clearErrorMessage]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSecciones = async () => {
|
||||
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO && idPublicacion) {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true);
|
||||
setSeccionesDisponibles(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar secciones:", error);
|
||||
setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.' }));
|
||||
setSeccionesDisponibles([]);
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
} else {
|
||||
setSeccionesDisponibles([]);
|
||||
// No es necesario setIdSeccion('') aquí si el useEffect de nuevoEstadoId ya lo hace.
|
||||
}
|
||||
};
|
||||
if (idPublicacion && Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { // Solo fetchear si hay idPublicacion
|
||||
fetchSecciones();
|
||||
} else {
|
||||
setSeccionesDisponibles([]);
|
||||
// No es necesario setIdSeccion('') aquí si el useEffect de nuevoEstadoId ya lo hace.
|
||||
setSeccionesDisponibles([]); // Limpiar si no se cumplen condiciones
|
||||
}
|
||||
}, [nuevoEstadoId, idPublicacion]);
|
||||
|
||||
|
||||
// Efecto para limpiar publicacion/seccion si el nuevo estado no es "En Uso"
|
||||
useEffect(() => {
|
||||
if (Number(nuevoEstadoId) !== ID_ESTADO_EN_USO) {
|
||||
setIdPublicacion('');
|
||||
setIdSeccion('');
|
||||
}
|
||||
}, [nuevoEstadoId]);
|
||||
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.';
|
||||
if (!fechaCambioEstado.trim()) {
|
||||
errors.fechaCambioEstado = 'La fecha es obligatoria.';
|
||||
} else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaCambioEstado)) {
|
||||
errors.fechaCambioEstado = 'Formato de fecha inválido.';
|
||||
} else if (bobinaActual) {
|
||||
const fechaRemitoSimple = bobinaActual.fechaRemito.split('T')[0];
|
||||
if (fechaCambioEstado < fechaRemitoSimple) {
|
||||
errors.fechaCambioEstado = `La fecha no puede ser anterior al ingreso (${fechaRemitoSimple}).`;
|
||||
}
|
||||
}
|
||||
|
||||
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO) {
|
||||
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.';
|
||||
if (!idSeccion) errors.idSeccion = 'Seleccione una sección.';
|
||||
}
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
// La lógica de limpieza de pub/secc se movió a un useEffect dedicado a nuevoEstadoId
|
||||
// y el de sección a un useEffect de idPublicacion
|
||||
if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion
|
||||
setIdSeccion('');
|
||||
}
|
||||
};
|
||||
if (idPublicacion && Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { // Solo fetchear si hay idPublicacion
|
||||
fetchSecciones();
|
||||
} else {
|
||||
setSeccionesDisponibles([]); // Limpiar si no se cumplen condiciones
|
||||
}
|
||||
}, [nuevoEstadoId, idPublicacion]);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate() || !bobinaActual) return;
|
||||
|
||||
// Efecto para limpiar publicacion/seccion si el nuevo estado no es "En Uso"
|
||||
useEffect(() => {
|
||||
if (Number(nuevoEstadoId) !== ID_ESTADO_EN_USO) {
|
||||
setIdPublicacion('');
|
||||
setIdSeccion('');
|
||||
}
|
||||
}, [nuevoEstadoId]);
|
||||
setLoading(true);
|
||||
try {
|
||||
const esEnUso = Number(nuevoEstadoId) === ID_ESTADO_EN_USO;
|
||||
const dataToSubmit: CambiarEstadoBobinaDto = {
|
||||
nuevoEstadoId: Number(nuevoEstadoId),
|
||||
idPublicacion: esEnUso && idPublicacion ? Number(idPublicacion) : null,
|
||||
idSeccion: esEnUso && idPublicacion && idSeccion ? Number(idSeccion) : null,
|
||||
obs: obs.trim() || null,
|
||||
fechaCambioEstado,
|
||||
};
|
||||
await onSubmit(bobinaActual.idBobina, dataToSubmit);
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de StockBobinaCambioEstadoModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!bobinaActual) return null;
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.';
|
||||
if (!fechaCambioEstado.trim()) errors.fechaCambioEstado = 'La fecha es obligatoria.';
|
||||
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaCambioEstado)) errors.fechaCambioEstado = 'Formato de fecha inválido.';
|
||||
|
||||
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO) {
|
||||
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.';
|
||||
if (!idSeccion) errors.idSeccion = 'Seleccione una sección.';
|
||||
}
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleInputChange = (fieldName: string) => {
|
||||
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null }));
|
||||
if (errorMessage) clearErrorMessage();
|
||||
// La lógica de limpieza de pub/secc se movió a un useEffect dedicado a nuevoEstadoId
|
||||
// y el de sección a un useEffect de idPublicacion
|
||||
if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion
|
||||
setIdSeccion('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
clearErrorMessage();
|
||||
if (!validate() || !bobinaActual) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const esEnUso = Number(nuevoEstadoId) === ID_ESTADO_EN_USO;
|
||||
const dataToSubmit: CambiarEstadoBobinaDto = {
|
||||
nuevoEstadoId: Number(nuevoEstadoId),
|
||||
idPublicacion: esEnUso && idPublicacion ? Number(idPublicacion) : null,
|
||||
idSeccion: esEnUso && idPublicacion && idSeccion ? Number(idSeccion) : null,
|
||||
obs: obs.trim() || null,
|
||||
fechaCambioEstado,
|
||||
};
|
||||
await onSubmit(bobinaActual.idBobina, dataToSubmit);
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
console.error("Error en submit de StockBobinaCambioEstadoModal:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!bobinaActual) return null;
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Cambiar Estado de Bobina: {bobinaActual.nroBobina}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Estado Actual: <strong>{bobinaActual.nombreEstadoBobina}</strong>
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.nuevoEstadoId} required>
|
||||
<InputLabel id="nuevo-estado-select-label">Nuevo Estado</InputLabel>
|
||||
<Select
|
||||
labelId="nuevo-estado-select-label"
|
||||
label="Nuevo Estado"
|
||||
value={nuevoEstadoId}
|
||||
onChange={(e) => {
|
||||
setNuevoEstadoId(e.target.value as number | string);
|
||||
handleInputChange('nuevoEstadoId');
|
||||
}}
|
||||
disabled={loading || loadingDropdowns || estadosDisponibles.length === 0}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione un estado</em></MenuItem>
|
||||
{estadosDisponibles.map((e) => (<MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.nuevoEstadoId && <Typography color="error" variant="caption">{localErrors.nuevoEstadoId}</Typography>}
|
||||
</FormControl>
|
||||
|
||||
{Number(nuevoEstadoId) === ID_ESTADO_EN_USO && (
|
||||
<>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
|
||||
<InputLabel id="publicacion-estado-select-label">Publicación</InputLabel>
|
||||
<Select labelId="publicacion-estado-select-label" label="Publicación" value={idPublicacion}
|
||||
onChange={(e) => {setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion');}}
|
||||
disabled={loading || loadingDropdowns || publicacionesDisponibles.length === 0}
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Cambiar Estado de Bobina: {bobinaActual.nroBobina}
|
||||
</Typography>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Estado Actual: <strong>{bobinaActual.nombreEstadoBobina}</strong>
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.nuevoEstadoId} required>
|
||||
<InputLabel id="nuevo-estado-select-label">Nuevo Estado</InputLabel>
|
||||
<Select
|
||||
labelId="nuevo-estado-select-label"
|
||||
label="Nuevo Estado"
|
||||
value={nuevoEstadoId}
|
||||
onChange={(e) => {
|
||||
setNuevoEstadoId(e.target.value as number | string);
|
||||
handleInputChange('nuevoEstadoId');
|
||||
}}
|
||||
disabled={loading || loadingDropdowns || estadosDisponibles.length === 0}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione publicación</em></MenuItem>
|
||||
{publicacionesDisponibles.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>))}
|
||||
<MenuItem value="" disabled><em>Seleccione un estado</em></MenuItem>
|
||||
{estadosDisponibles.map((e) => (<MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
|
||||
{localErrors.nuevoEstadoId && <Typography color="error" variant="caption">{localErrors.nuevoEstadoId}</Typography>}
|
||||
</FormControl>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idSeccion} required>
|
||||
<InputLabel id="seccion-estado-select-label">Sección</InputLabel>
|
||||
<Select labelId="seccion-estado-select-label" label="Sección" value={idSeccion}
|
||||
onChange={(e) => {setIdSeccion(e.target.value as number); handleInputChange('idSeccion');}}
|
||||
disabled={loading || loadingDropdowns || !idPublicacion || seccionesDisponibles.length === 0}
|
||||
>
|
||||
<MenuItem value="" disabled><em>{idPublicacion ? (seccionesDisponibles.length > 0 ? 'Seleccione sección' : 'No hay secciones para esta pub.') : 'Seleccione publicación primero'}</em></MenuItem>
|
||||
{seccionesDisponibles.map((s) => (<MenuItem key={s.idSeccion} value={s.idSeccion}>{s.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idSeccion && <Typography color="error" variant="caption">{localErrors.idSeccion}</Typography>}
|
||||
{localErrors.secciones && <Alert severity="warning" sx={{mt:0.5}}>{localErrors.secciones}</Alert>}
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextField label="Fecha Cambio de Estado" type="date" value={fechaCambioEstado} required
|
||||
onChange={(e) => {setFechaCambioEstado(e.target.value); handleInputChange('fechaCambioEstado');}}
|
||||
margin="dense" fullWidth error={!!localErrors.fechaCambioEstado} helperText={localErrors.fechaCambioEstado || ''}
|
||||
disabled={loading} InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField label="Observaciones (Opcional)" value={obs}
|
||||
onChange={(e) => setObs(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={3} disabled={loading}
|
||||
/>
|
||||
{Number(nuevoEstadoId) === ID_ESTADO_EN_USO && (
|
||||
<>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
|
||||
<InputLabel id="publicacion-estado-select-label">Publicación</InputLabel>
|
||||
<Select labelId="publicacion-estado-select-label" label="Publicación" value={idPublicacion}
|
||||
onChange={(e) => { setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion'); }}
|
||||
disabled={loading || loadingDropdowns || publicacionesDisponibles.length === 0}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione publicación</em></MenuItem>
|
||||
{publicacionesDisponibles.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>}
|
||||
</FormControl>
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idSeccion} required>
|
||||
<InputLabel id="seccion-estado-select-label">Sección</InputLabel>
|
||||
<Select labelId="seccion-estado-select-label" label="Sección" value={idSeccion}
|
||||
onChange={(e) => { setIdSeccion(e.target.value as number); handleInputChange('idSeccion'); }}
|
||||
disabled={loading || loadingDropdowns || !idPublicacion || seccionesDisponibles.length === 0}
|
||||
>
|
||||
<MenuItem value="" disabled><em>{idPublicacion ? (seccionesDisponibles.length > 0 ? 'Seleccione sección' : 'No hay secciones para esta pub.') : 'Seleccione publicación primero'}</em></MenuItem>
|
||||
{seccionesDisponibles.map((s) => (<MenuItem key={s.idSeccion} value={s.idSeccion}>{s.nombre}</MenuItem>))}
|
||||
</Select>
|
||||
{localErrors.idSeccion && <Typography color="error" variant="caption">{localErrors.idSeccion}</Typography>}
|
||||
{localErrors.secciones && <Alert severity="warning" sx={{ mt: 0.5 }}>{localErrors.secciones}</Alert>}
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Fecha Cambio de Estado"
|
||||
type="date"
|
||||
value={fechaCambioEstado}
|
||||
required
|
||||
onChange={(e) => { setFechaCambioEstado(e.target.value); handleInputChange('fechaCambioEstado'); }}
|
||||
margin="dense"
|
||||
fullWidth
|
||||
error={!!localErrors.fechaCambioEstado}
|
||||
helperText={localErrors.fechaCambioEstado || ''}
|
||||
disabled={loading}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
inputProps={{
|
||||
min: bobinaActual?.fechaRemito.split('T')[0]
|
||||
}}
|
||||
/>
|
||||
<TextField label="Observaciones (Opcional)" value={obs}
|
||||
onChange={(e) => setObs(e.target.value)}
|
||||
margin="dense" fullWidth multiline rows={3} disabled={loading}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained"
|
||||
disabled={loading || loadingDropdowns || (estadosDisponibles.length === 0 && bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO)}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Guardar Cambio de Estado'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>}
|
||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained"
|
||||
disabled={loading || loadingDropdowns || (estadosDisponibles.length === 0 && bobinaActual.idEstadoBobina === ID_ESTADO_EN_USO) }>
|
||||
{loading ? <CircularProgress size={24} /> : 'Guardar Cambio de Estado'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockBobinaCambioEstadoModal;
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert } from '@mui/material';
|
||||
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
||||
import type { UpdateFechaRemitoLoteDto } from '../../../models/dtos/Impresion/UpdateFechaRemitoLoteDto';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%', left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '90%', sm: 450 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000', boxShadow: 24, p: 4
|
||||
};
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: UpdateFechaRemitoLoteDto) => Promise<void>;
|
||||
bobinaContexto: StockBobinaDto | null;
|
||||
errorMessage: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const StockBobinaFechaRemitoModal: React.FC<Props> = ({ open, onClose, onSubmit, bobinaContexto, errorMessage, clearErrorMessage }) => {
|
||||
const [nuevaFecha, setNuevaFecha] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && bobinaContexto) {
|
||||
setNuevaFecha(bobinaContexto.fechaRemito.split('T')[0]); // Iniciar con la fecha actual
|
||||
setLocalError(null);
|
||||
clearErrorMessage();
|
||||
}
|
||||
}, [open, bobinaContexto, clearErrorMessage]);
|
||||
|
||||
if (!bobinaContexto) return null;
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!nuevaFecha) {
|
||||
setLocalError('Debe seleccionar una nueva fecha.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const data: UpdateFechaRemitoLoteDto = {
|
||||
idPlanta: bobinaContexto.idPlanta,
|
||||
remito: bobinaContexto.remito,
|
||||
fechaRemitoActual: bobinaContexto.fechaRemito.split('T')[0],
|
||||
nuevaFechaRemito: nuevaFecha
|
||||
};
|
||||
await onSubmit(data);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
// El error de la API es manejado por el prop `errorMessage`
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6" component="h2">Corregir Fecha de Remito</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
Esto cambiará la fecha para <strong>todas</strong> las bobinas del remito <strong>{bobinaContexto.remito}</strong> en la planta <strong>{bobinaContexto.nombrePlanta}</strong>.
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField label="Fecha Actual" value={new Date(bobinaContexto.fechaRemito).toLocaleDateString('es-AR', { timeZone: 'UTC' })} disabled fullWidth margin="normal" />
|
||||
<TextField label="Nueva Fecha de Remito" type="date" value={nuevaFecha}
|
||||
onChange={e => { setNuevaFecha(e.target.value); setLocalError(null); }}
|
||||
required fullWidth margin="normal" InputLabelProps={{ shrink: true }}
|
||||
error={!!localError} helperText={localError} autoFocus
|
||||
/>
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Guardar Nueva Fecha'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockBobinaFechaRemitoModal;
|
||||
@@ -0,0 +1,374 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, Stepper, Step, StepLabel,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton, Divider,
|
||||
InputAdornment, Tooltip
|
||||
} from '@mui/material';
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
|
||||
import type { CreateStockBobinaLoteDto } from '../../../models/dtos/Impresion/CreateStockBobinaLoteDto';
|
||||
import type { BobinaLoteDetalleDto } from '../../../models/dtos/Impresion/BobinaLoteDetalleDto';
|
||||
import type { PlantaDto } from '../../../models/dtos/Impresion/PlantaDto';
|
||||
import type { TipoBobinaDto } from '../../../models/dtos/Impresion/TipoBobinaDto';
|
||||
import stockBobinaService from '../../../services/Impresion/stockBobinaService';
|
||||
import plantaService from '../../../services/Impresion/plantaService';
|
||||
import tipoBobinaService from '../../../services/Impresion/tipoBobinaService';
|
||||
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute' as 'absolute',
|
||||
top: '50%', left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: { xs: '95%', sm: '80%', md: '900px' },
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000', boxShadow: 24, p: 3,
|
||||
maxHeight: '90vh', overflowY: 'auto', display: 'flex', flexDirection: 'column'
|
||||
};
|
||||
|
||||
interface NuevaBobinaState extends BobinaLoteDetalleDto {
|
||||
idTemporal: string;
|
||||
}
|
||||
|
||||
interface StockBobinaLoteFormModalProps {
|
||||
open: boolean;
|
||||
onClose: (refrescar: boolean) => void;
|
||||
}
|
||||
|
||||
const steps = ['Datos del Remito', 'Ingreso de Bobinas'];
|
||||
|
||||
const StockBobinaLoteFormModal: React.FC<StockBobinaLoteFormModalProps> = ({ open, onClose }) => {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [apiError, setApiError] = useState<string | null>(null);
|
||||
|
||||
// Step 1 State
|
||||
const [idPlanta, setIdPlanta] = useState<number | ''>('');
|
||||
const [remito, setRemito] = useState('');
|
||||
const [fechaRemito, setFechaRemito] = useState(new Date().toISOString().split('T')[0]);
|
||||
const [headerErrors, setHeaderErrors] = useState<{ [key: string]: string }>({});
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const [remitoStatusMessage, setRemitoStatusMessage] = useState<string | null>(null);
|
||||
const [remitoStatusSeverity, setRemitoStatusSeverity] = useState<'success' | 'info'>('info');
|
||||
const [isDateAutocompleted, setIsDateAutocompleted] = useState(false);
|
||||
|
||||
// Step 2 State
|
||||
const [bobinasExistentes, setBobinasExistentes] = useState<StockBobinaDto[]>([]);
|
||||
const [nuevasBobinas, setNuevasBobinas] = useState<NuevaBobinaState[]>([]);
|
||||
const [detalleErrors, setDetalleErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
// Dropdowns data
|
||||
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
|
||||
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(true);
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setActiveStep(0); setLoading(false); setApiError(null);
|
||||
setIdPlanta(''); setRemito(''); setFechaRemito(new Date().toISOString().split('T')[0]);
|
||||
setHeaderErrors({}); setBobinasExistentes([]); setNuevasBobinas([]); setDetalleErrors({});
|
||||
setRemitoStatusMessage(null);
|
||||
setIsDateAutocompleted(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDropdowns = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [plantasData, tiposData] = await Promise.all([
|
||||
plantaService.getAllPlantas(),
|
||||
tipoBobinaService.getAllTiposBobina()
|
||||
]);
|
||||
setPlantas(plantasData);
|
||||
setTiposBobina(tiposData);
|
||||
} catch (error) {
|
||||
setApiError("Error al cargar datos necesarios (plantas, tipos).");
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
if (open) {
|
||||
fetchDropdowns();
|
||||
} else {
|
||||
resetState();
|
||||
}
|
||||
}, [open, resetState]);
|
||||
|
||||
useEffect(() => {
|
||||
const verificarRemitoParaAutocompletar = async () => {
|
||||
setRemitoStatusMessage(null);
|
||||
if (remito.trim() && idPlanta) {
|
||||
setIsVerifying(true);
|
||||
try {
|
||||
const existentes = await stockBobinaService.verificarRemitoExistente(Number(idPlanta), remito.trim());
|
||||
if (existentes.length > 0) {
|
||||
setFechaRemito(existentes[0].fechaRemito.split('T')[0]);
|
||||
setRemitoStatusMessage("Remito existente. Se autocompletó la fecha.");
|
||||
setRemitoStatusSeverity('info');
|
||||
setIsDateAutocompleted(true);
|
||||
} else {
|
||||
setRemitoStatusMessage("Este es un remito nuevo.");
|
||||
setRemitoStatusSeverity('success');
|
||||
setIsDateAutocompleted(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Fallo la verificación automática de remito: ", error);
|
||||
setRemitoStatusMessage("No se pudo verificar el remito.");
|
||||
setRemitoStatusSeverity('info');
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
const handler = setTimeout(() => {
|
||||
verificarRemitoParaAutocompletar();
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [idPlanta, remito]);
|
||||
|
||||
const handleClose = () => onClose(false);
|
||||
|
||||
const handleNext = async () => {
|
||||
const errors: { [key: string]: string } = {};
|
||||
if (!remito.trim()) errors.remito = "El número de remito es obligatorio.";
|
||||
if (!idPlanta) errors.idPlanta = "Seleccione una planta.";
|
||||
if (!fechaRemito) errors.fechaRemito = "La fecha es obligatoria.";
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setHeaderErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); setApiError(null);
|
||||
try {
|
||||
const existentes = await stockBobinaService.verificarRemitoExistente(Number(idPlanta), remito, fechaRemito);
|
||||
setBobinasExistentes(existentes);
|
||||
setActiveStep(1);
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || "Error al verificar el remito.";
|
||||
setApiError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => setActiveStep(0);
|
||||
|
||||
const handleAddBobina = () => {
|
||||
setNuevasBobinas(prev => [...prev, {
|
||||
idTemporal: crypto.randomUUID(), idTipoBobina: 0, nroBobina: '', peso: 0
|
||||
}]);
|
||||
setTimeout(() => {
|
||||
tableContainerRef.current?.scrollTo({ top: tableContainerRef.current.scrollHeight, behavior: 'smooth' });
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleRemoveBobina = (idTemporal: string) => {
|
||||
setNuevasBobinas(prev => prev.filter(b => b.idTemporal !== idTemporal));
|
||||
};
|
||||
|
||||
const handleBobinaChange = (idTemporal: string, field: keyof NuevaBobinaState, value: any) => {
|
||||
setNuevasBobinas(prev => prev.map(b => b.idTemporal === idTemporal ? { ...b, [field]: value } : b));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const errors: { [key: string]: string } = {};
|
||||
if (nuevasBobinas.length === 0) {
|
||||
setApiError("Debe agregar al menos una nueva bobina para guardar.");
|
||||
return;
|
||||
}
|
||||
|
||||
const todosNrosBobina = new Set(bobinasExistentes.map(b => b.nroBobina));
|
||||
nuevasBobinas.forEach(b => {
|
||||
if (!b.idTipoBobina) errors[b.idTemporal + '_tipo'] = "Requerido";
|
||||
if (!b.nroBobina.trim()) errors[b.idTemporal + '_nro'] = "Requerido";
|
||||
if ((b.peso || 0) <= 0) errors[b.idTemporal + '_peso'] = "Inválido";
|
||||
if (todosNrosBobina.has(b.nroBobina.trim())) errors[b.idTemporal + '_nro'] = "Duplicado";
|
||||
todosNrosBobina.add(b.nroBobina.trim());
|
||||
});
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setDetalleErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); setApiError(null);
|
||||
try {
|
||||
const lote: CreateStockBobinaLoteDto = {
|
||||
idPlanta: Number(idPlanta),
|
||||
remito: remito.trim(),
|
||||
fechaRemito,
|
||||
bobinas: nuevasBobinas.map(({ idTipoBobina, nroBobina, peso }) => ({
|
||||
idTipoBobina: Number(idTipoBobina), nroBobina: nroBobina.trim(), peso: Number(peso)
|
||||
}))
|
||||
};
|
||||
await stockBobinaService.ingresarLoteBobinas(lote);
|
||||
onClose(true);
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || "Error al guardar el lote de bobinas.";
|
||||
setApiError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const renderStepContent = (step: number) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h6">Datos de Cabecera</Typography>
|
||||
<TextField label="Número de Remito" value={remito}
|
||||
onChange={e => {
|
||||
setRemito(e.target.value);
|
||||
setIsDateAutocompleted(false);
|
||||
}}
|
||||
required error={!!headerErrors.remito} helperText={headerErrors.remito} disabled={loading} autoFocus
|
||||
/>
|
||||
<FormControl fullWidth error={!!headerErrors.idPlanta}>
|
||||
<InputLabel id="planta-label" required>Planta de Destino</InputLabel>
|
||||
<Select labelId="planta-label" value={idPlanta} label="Planta de Destino"
|
||||
onChange={e => {
|
||||
setIdPlanta(e.target.value as number);
|
||||
setIsDateAutocompleted(false);
|
||||
}}
|
||||
disabled={loading || loadingDropdowns}
|
||||
endAdornment={isVerifying && (<InputAdornment position="end" sx={{ mr: 2 }}><CircularProgress size={20} /></InputAdornment>)}
|
||||
>
|
||||
{plantas.map(p => <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
{headerErrors.idPlanta && <Typography color="error" variant="caption">{headerErrors.idPlanta}</Typography>}
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="Fecha de Remito"
|
||||
type="date"
|
||||
value={fechaRemito}
|
||||
onChange={e => setFechaRemito(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
required
|
||||
error={!!headerErrors.fechaRemito}
|
||||
helperText={headerErrors.fechaRemito}
|
||||
disabled={loading || isDateAutocompleted}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
isDateAutocompleted && (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip title="Editar fecha">
|
||||
<IconButton onClick={() => setIsDateAutocompleted(false)} edge="end">
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{ minHeight: 48, mt: 1 }}>
|
||||
{remitoStatusMessage && !isVerifying && (
|
||||
<Alert severity={remitoStatusSeverity} icon={false} variant="outlined">
|
||||
{remitoStatusMessage}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
{bobinasExistentes.length > 0 && (
|
||||
<>
|
||||
<Typography variant="subtitle1" gutterBottom>Bobinas ya ingresadas para este remito:</Typography>
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: '150px', mb: 2 }}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead><TableRow><TableCell>Nro. Bobina</TableCell><TableCell>Tipo</TableCell><TableCell align="right">Peso (Kg)</TableCell></TableRow></TableHead>
|
||||
<TableBody>
|
||||
{bobinasExistentes.map(b => (
|
||||
<TableRow key={b.idBobina}><TableCell>{b.nroBobina}</TableCell><TableCell>{b.nombreTipoBobina}</TableCell><TableCell align="right">{b.peso}</TableCell></TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)}
|
||||
<Typography variant="h6">Nuevas Bobinas a Ingresar</Typography>
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ flexGrow: 1, my: 1, minHeight: '150px' }} ref={tableContainerRef}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead><TableRow><TableCell sx={{ minWidth: 200 }}>Tipo Bobina</TableCell><TableCell>Nro. Bobina</TableCell><TableCell>Peso (Kg)</TableCell><TableCell></TableCell></TableRow></TableHead>
|
||||
<TableBody>
|
||||
{nuevasBobinas.map(bobina => (
|
||||
<TableRow key={bobina.idTemporal}>
|
||||
<TableCell><FormControl fullWidth size="small" error={!!detalleErrors[bobina.idTemporal + '_tipo']}><Select value={bobina.idTipoBobina} onChange={e => handleBobinaChange(bobina.idTemporal, 'idTipoBobina', e.target.value)} disabled={loadingDropdowns}><MenuItem value={0} disabled>Seleccione</MenuItem>{tiposBobina.map(t => <MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>)}</Select></FormControl></TableCell>
|
||||
<TableCell><TextField fullWidth size="small" value={bobina.nroBobina} onChange={e => handleBobinaChange(bobina.idTemporal, 'nroBobina', e.target.value)} error={!!detalleErrors[bobina.idTemporal + '_nro']} helperText={detalleErrors[bobina.idTemporal + '_nro']} /></TableCell>
|
||||
<TableCell><TextField fullWidth size="small" type="number" value={bobina.peso || ''} onChange={e => handleBobinaChange(bobina.idTemporal, 'peso', e.target.value)} error={!!detalleErrors[bobina.idTemporal + '_peso']} helperText={detalleErrors[bobina.idTemporal + '_peso']} /></TableCell>
|
||||
<TableCell><IconButton size="small" color="error" onClick={() => handleRemoveBobina(bobina.idTemporal)}><DeleteOutlineIcon /></IconButton></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Button startIcon={<AddCircleOutlineIcon />} onClick={handleAddBobina} sx={{ mt: 1 }}>Agregar Bobina</Button>
|
||||
</Box>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={handleClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
top: 8,
|
||||
color: (theme) => theme.palette.grey[500],
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant="h5" component="h2" gutterBottom>Ingreso de Bobinas por Lote</Typography>
|
||||
|
||||
<Stepper activeStep={activeStep} sx={{ mb: 2 }}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
|
||||
<Box sx={{ flexGrow: 1, overflowY: 'auto' }}>
|
||||
{loadingDropdowns && activeStep === 0 ? <Box sx={{ display: 'flex', justifyContent: 'center', my: 5 }}><CircularProgress /></Box> : renderStepContent(activeStep)}
|
||||
</Box>
|
||||
|
||||
{apiError && <Alert severity="error" sx={{ mt: 2, flexShrink: 0 }}>{apiError}</Alert>}
|
||||
|
||||
<Divider sx={{ my: 2, flexShrink: 0 }} />
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', pt: 1, flexShrink: 0 }}>
|
||||
<Button color="inherit" disabled={activeStep === 0 || loading} onClick={handleBack}>
|
||||
Atrás
|
||||
</Button>
|
||||
<Box>
|
||||
{activeStep === 0 && <Button onClick={handleNext} variant="contained" disabled={loading || loadingDropdowns}>{loading ? <CircularProgress size={24} /> : 'Verificar y Continuar'}</Button>}
|
||||
{activeStep === 1 && <Button onClick={handleSubmit} variant="contained" color="success" disabled={loading}>{loading ? <CircularProgress size={24} /> : 'Guardar Lote'}</Button>}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockBobinaLoteFormModal;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent, InputAdornment } from '@mui/material';
|
||||
import type { FacturaDto } from '../../../models/dtos/Suscripciones/FacturaDto';
|
||||
import type { FacturaConsolidadaDto } from '../../../models/dtos/Suscripciones/ResumenCuentaSuscriptorDto';
|
||||
import type { CreatePagoDto } from '../../../models/dtos/Suscripciones/CreatePagoDto';
|
||||
import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto';
|
||||
import formaPagoService from '../../../services/Suscripciones/formaPagoService';
|
||||
@@ -23,17 +23,19 @@ interface PagoManualModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreatePagoDto) => Promise<void>;
|
||||
factura: FacturaDto | null;
|
||||
factura: FacturaConsolidadaDto | null;
|
||||
nombreSuscriptor: string; // Se pasa el nombre del suscriptor como prop-
|
||||
errorMessage?: string | null;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, errorMessage, clearErrorMessage }) => {
|
||||
const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, nombreSuscriptor, errorMessage, clearErrorMessage }) => {
|
||||
const [formData, setFormData] = useState<Partial<CreatePagoDto>>({});
|
||||
const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingFormasPago, setLoadingFormasPago] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
const saldoPendiente = factura ? factura.importeFinal - factura.totalPagado : 0;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFormasDePago = async () => {
|
||||
@@ -52,12 +54,12 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
||||
fetchFormasDePago();
|
||||
setFormData({
|
||||
idFactura: factura.idFactura,
|
||||
monto: factura.saldoPendiente, // Usar el saldo pendiente como valor por defecto
|
||||
monto: saldoPendiente,
|
||||
fechaPago: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
setLocalErrors({});
|
||||
}
|
||||
}, [open, factura]);
|
||||
}, [open, factura, saldoPendiente]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
@@ -65,13 +67,11 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
||||
if (!formData.fechaPago) errors.fechaPago = "La fecha de pago es obligatoria.";
|
||||
|
||||
const monto = formData.monto ?? 0;
|
||||
const saldo = factura?.saldoPendiente ?? 0;
|
||||
|
||||
if (monto <= 0) {
|
||||
errors.monto = "El monto debe ser mayor a cero.";
|
||||
} else if (monto > saldo) {
|
||||
// Usamos toFixed(2) para mostrar el formato de moneda correcto en el mensaje
|
||||
errors.monto = `El monto no puede superar el saldo pendiente de $${saldo.toFixed(2)}.`;
|
||||
errors.monto = "El monto debe ser mayor a cero.";
|
||||
} else if (monto > saldoPendiente) {
|
||||
errors.monto = `El monto no puede superar el saldo pendiente de $${saldoPendiente.toFixed(2)}.`;
|
||||
}
|
||||
|
||||
setLocalErrors(errors);
|
||||
@@ -117,29 +117,32 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Typography variant="h6">Registrar Pago Manual</Typography>
|
||||
<Typography variant="subtitle1" gutterBottom sx={{fontWeight: 'bold'}}>
|
||||
Saldo Pendiente: ${factura.saldoPendiente.toFixed(2)}
|
||||
<Typography variant="body1" color="text.secondary" gutterBottom>
|
||||
Para: {nombreSuscriptor}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 'bold' }}>
|
||||
Saldo Pendiente: ${saldoPendiente.toFixed(2)}
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
||||
<TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} />
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idFormaPago}>
|
||||
<InputLabel id="forma-pago-label" required>Forma de Pago</InputLabel>
|
||||
<Select name="idFormaPago" labelId="forma-pago-label" value={formData.idFormaPago || ''} onChange={handleSelectChange} label="Forma de Pago" disabled={loadingFormasPago}>
|
||||
{formasDePago.map(fp => <MenuItem key={fp.idFormaPago} value={fp.idFormaPago}>{fp.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField name="monto" label="Monto Pagado" type="number" value={formData.monto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.monto} helperText={localErrors.monto} InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} inputProps={{ step: "0.01" }} />
|
||||
<TextField name="referencia" label="Referencia (Opcional)" value={formData.referencia || ''} onChange={handleInputChange} fullWidth margin="dense" />
|
||||
<TextField name="observaciones" label="Observaciones (Opcional)" value={formData.observaciones || ''} onChange={handleInputChange} fullWidth margin="dense" multiline rows={2} />
|
||||
<TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} />
|
||||
<FormControl fullWidth margin="dense" error={!!localErrors.idFormaPago}>
|
||||
<InputLabel id="forma-pago-label" required>Forma de Pago</InputLabel>
|
||||
<Select name="idFormaPago" labelId="forma-pago-label" value={formData.idFormaPago || ''} onChange={handleSelectChange} label="Forma de Pago" disabled={loadingFormasPago}>
|
||||
{formasDePago.map(fp => <MenuItem key={fp.idFormaPago} value={fp.idFormaPago}>{fp.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField name="monto" label="Monto Pagado" type="number" value={formData.monto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.monto} helperText={localErrors.monto} InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} inputProps={{ step: "0.01" }} />
|
||||
<TextField name="referencia" label="Referencia (Opcional)" value={formData.referencia || ''} onChange={handleInputChange} fullWidth margin="dense" />
|
||||
<TextField name="observaciones" label="Observaciones (Opcional)" value={formData.observaciones || ''} onChange={handleInputChange} fullWidth margin="dense" multiline rows={2} />
|
||||
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
|
||||
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading || loadingFormasPago}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Registrar Pago'}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
|
||||
<Button type="submit" variant="contained" disabled={loading || loadingFormasPago}>
|
||||
{loading ? <CircularProgress size={24} /> : 'Registrar Pago'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
@@ -14,18 +14,18 @@ const SECCION_PERMISSIONS_PREFIX = "SS";
|
||||
// Mapeo de codAcc de sección a su módulo conceptual
|
||||
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
|
||||
if (codAcc === "SS001") return "Distribución";
|
||||
if (codAcc === "SS007") return "Suscripciones";
|
||||
if (codAcc === "SS002") return "Contables";
|
||||
if (codAcc === "SS003") return "Impresión";
|
||||
if (codAcc === "SS004") return "Reportes";
|
||||
if (codAcc === "SS005") return "Radios";
|
||||
if (codAcc === "SS006") return "Usuarios";
|
||||
if (codAcc === "SS005") return "Radios";
|
||||
return null;
|
||||
};
|
||||
|
||||
// Función para determinar el módulo conceptual de un permiso individual
|
||||
const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
const moduloLower = permisoModulo.toLowerCase();
|
||||
|
||||
if (moduloLower.includes("distribuidores") ||
|
||||
moduloLower.includes("canillas") || // Cubre "Canillas" y "Movimientos Canillas"
|
||||
moduloLower.includes("publicaciones distribución") ||
|
||||
@@ -36,6 +36,9 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
moduloLower.includes("ctrl. devoluciones")) {
|
||||
return "Distribución";
|
||||
}
|
||||
if (moduloLower.includes("suscripciones")) {
|
||||
return "Suscripciones";
|
||||
}
|
||||
if (moduloLower.includes("cuentas pagos") ||
|
||||
moduloLower.includes("cuentas notas") ||
|
||||
moduloLower.includes("cuentas tipos pagos")) {
|
||||
@@ -89,7 +92,7 @@ const PermisosChecklist: React.FC<PermisosChecklistProps> = ({
|
||||
return acc;
|
||||
}, {} as Record<string, PermisoAsignadoDto[]>);
|
||||
|
||||
const ordenModulosPrincipales = ["Distribución", "Contables", "Impresión", "Radios", "Usuarios", "Reportes", "Permisos (Definición)"];
|
||||
const ordenModulosPrincipales = ["Distribución", "Suscripciones", "Contables", "Impresión", "Usuarios", "Reportes", "Radios","Permisos (Definición)"];
|
||||
// Añadir módulos que solo tienen permiso de sección (como Radios) pero no hijos (aún)
|
||||
permisosDeSeccion.forEach(ps => {
|
||||
const moduloConceptual = getModuloFromSeccionCodAcc(ps.codAcc);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox
|
||||
} from '@mui/material';
|
||||
import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto';
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
// src/hooks/usePermissions.ts
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const usePermissions = () => {
|
||||
const { user } = useAuth(); // user aquí es de tipo UserContextData | null
|
||||
const { user } = useAuth();
|
||||
|
||||
const tienePermiso = (codigoPermisoRequerido: string): boolean => {
|
||||
if (!user) { // Si no hay usuario logueado
|
||||
// Envolvemos la función en useCallback.
|
||||
// Su dependencia es [user], por lo que la función solo se
|
||||
// volverá a crear si el objeto 'user' cambia (ej. al iniciar/cerrar sesión).
|
||||
const tienePermiso = useCallback((codigoPermisoRequerido: string): boolean => {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
if (user.esSuperAdmin) { // SuperAdmin tiene todos los permisos
|
||||
if (user.esSuperAdmin) {
|
||||
return true;
|
||||
}
|
||||
// Verificar si la lista de permisos del usuario incluye el código requerido
|
||||
return user.permissions?.includes(codigoPermisoRequerido) ?? false;
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
// También puede exportar el objeto user completo si se necesita en otros lugares
|
||||
// o propiedades específicas como idPerfil, esSuperAdmin.
|
||||
return {
|
||||
tienePermiso,
|
||||
isSuperAdmin: user?.esSuperAdmin ?? false,
|
||||
|
||||
@@ -12,4 +12,6 @@ export interface DistribuidorDto {
|
||||
telefono?: string | null;
|
||||
email?: string | null;
|
||||
localidad?: string | null;
|
||||
baja?: boolean;
|
||||
fechaBaja?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface BobinaLoteDetalleDto {
|
||||
idTipoBobina: number;
|
||||
nroBobina: string;
|
||||
peso: number;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { BobinaLoteDetalleDto } from './BobinaLoteDetalleDto';
|
||||
|
||||
export interface CreateStockBobinaLoteDto {
|
||||
idPlanta: number;
|
||||
remito: string;
|
||||
fechaRemito: string; // "yyyy-MM-dd"
|
||||
bobinas: BobinaLoteDetalleDto[];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface UpdateFechaRemitoLoteDto {
|
||||
idPlanta: number;
|
||||
remito: string;
|
||||
fechaRemitoActual: string; // "yyyy-MM-dd"
|
||||
nuevaFechaRemito: string; // "yyyy-MM-dd"
|
||||
}
|
||||
@@ -12,6 +12,8 @@ export interface FacturaConsolidadaDto {
|
||||
estadoPago: string;
|
||||
estadoFacturacion: string;
|
||||
numeroFactura?: string | null;
|
||||
totalPagado: number;
|
||||
tipoFactura: 'Mensual' | 'Alta';
|
||||
detalles: FacturaDetalleDto[];
|
||||
// Añadimos el id del suscriptor para que sea fácil pasarlo a los handlers
|
||||
idSuscriptor: number;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
@@ -28,11 +28,11 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||
const [pagos, setPagos] = useState<PagoDistribuidorDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
const [pageApiErrorMessage, setPageApiErrorMessage] = useState<string | null>(null);
|
||||
const [modalApiErrorMessage, setModalApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Filtros
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);//useState('');
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
|
||||
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
|
||||
const [filtroTipoMov, setFiltroTipoMov] = useState<'Recibido' | 'Realizado' | ''>('');
|
||||
@@ -50,7 +50,6 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||
const [selectedRow, setSelectedRow] = useState<PagoDistribuidorDto | null>(null);
|
||||
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
// Permisos CP001 (Ver), CP002 (Crear), CP003 (Modificar), CP004 (Eliminar)
|
||||
const puedeVer = isSuperAdmin || tienePermiso("CP001");
|
||||
const puedeCrear = isSuperAdmin || tienePermiso("CP002");
|
||||
const puedeModificar = isSuperAdmin || tienePermiso("CP003");
|
||||
@@ -59,21 +58,27 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||
const fetchFiltersDropdownData = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
const [distData, empData] = await Promise.all([
|
||||
distribuidorService.getAllDistribuidores(),
|
||||
empresaService.getAllEmpresas()
|
||||
]);
|
||||
setDistribuidores(distData);
|
||||
setEmpresas(empData);
|
||||
} catch (err) { console.error(err); setError("Error al cargar opciones de filtro.");
|
||||
const [distData, empData] = await Promise.all([
|
||||
distribuidorService.getAllDistribuidores(),
|
||||
empresaService.getAllEmpresas()
|
||||
]);
|
||||
setDistribuidores(distData);
|
||||
setEmpresas(empData);
|
||||
} catch (err) {
|
||||
console.error(err); setError("Error al cargar opciones de filtro.");
|
||||
} finally { setLoadingFiltersDropdown(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
|
||||
|
||||
const clearModalApiErrorMessage = useCallback(() => {
|
||||
setModalApiErrorMessage(null);
|
||||
}, []);
|
||||
|
||||
|
||||
const cargarPagos = useCallback(async () => {
|
||||
if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; }
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
setLoading(true); setError(null); setPageApiErrorMessage(null);
|
||||
try {
|
||||
const params = {
|
||||
fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null,
|
||||
@@ -83,19 +88,20 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||
};
|
||||
const data = await pagoDistribuidorService.getAllPagosDistribuidor(params);
|
||||
setPagos(data);
|
||||
} catch (err) { console.error(err); setError('Error al cargar los pagos.');
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los pagos.');
|
||||
} finally { setLoading(false); }
|
||||
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdDistribuidor, filtroIdEmpresa, filtroTipoMov]);
|
||||
|
||||
useEffect(() => { cargarPagos(); }, [cargarPagos]);
|
||||
|
||||
const handleOpenModal = (item?: PagoDistribuidorDto) => {
|
||||
setEditingPago(item || null); setApiErrorMessage(null); setModalOpen(true);
|
||||
setEditingPago(item || null); setModalApiErrorMessage(null); setModalOpen(true);
|
||||
};
|
||||
const handleCloseModal = () => { setModalOpen(false); setEditingPago(null); };
|
||||
|
||||
const handleSubmitModal = async (data: CreatePagoDistribuidorDto | UpdatePagoDistribuidorDto, idPago?: number) => {
|
||||
setApiErrorMessage(null);
|
||||
setModalApiErrorMessage(null);
|
||||
try {
|
||||
if (idPago && editingPago) {
|
||||
await pagoDistribuidorService.updatePagoDistribuidor(idPago, data as UpdatePagoDistribuidorDto);
|
||||
@@ -105,15 +111,19 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||
cargarPagos();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el pago.';
|
||||
setApiErrorMessage(message); throw err;
|
||||
setModalApiErrorMessage(message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (idPago: number) => {
|
||||
if (window.confirm(`¿Seguro de eliminar este pago (ID: ${idPago})? Esta acción revertirá el impacto en el saldo.`)) {
|
||||
setApiErrorMessage(null);
|
||||
try { await pagoDistribuidorService.deletePagoDistribuidor(idPago); cargarPagos(); }
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
|
||||
setPageApiErrorMessage(null);
|
||||
try { await pagoDistribuidorService.deletePagoDistribuidor(idPago); cargarPagos(); }
|
||||
catch (err: any) {
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
|
||||
setPageApiErrorMessage(msg);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
@@ -128,7 +138,15 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
};
|
||||
const displayData = pagos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
|
||||
const formatDate = (dateString?: string | null): string => {
|
||||
if (!dateString) return '-';
|
||||
const datePart = dateString.split('T')[0];
|
||||
const parts = datePart.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
}
|
||||
return datePart;
|
||||
};
|
||||
|
||||
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
|
||||
@@ -136,93 +154,96 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="h5" gutterBottom>Pagos de Distribuidores</Typography>
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small"/></Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2}}>
|
||||
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
|
||||
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/>
|
||||
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Distribuidor</InputLabel>
|
||||
<Select value={filtroIdDistribuidor} label="Distribuidor" onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
{distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Empresa (Saldo)</InputLabel>
|
||||
<Select value={filtroIdEmpresa} label="Empresa (Saldo)" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todas</em></MenuItem>
|
||||
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{minWidth: 150, flexGrow: 1}}>
|
||||
<InputLabel>Tipo Mov.</InputLabel>
|
||||
<Select value={filtroTipoMov} label="Tipo Mov." onChange={(e) => setFiltroTipoMov(e.target.value as 'Recibido' | 'Realizado' | '')}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
<MenuItem value="Recibido">Recibido</MenuItem>
|
||||
<MenuItem value="Realizado">Realizado</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Pago</Button>)}
|
||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
||||
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
|
||||
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
|
||||
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Distribuidor</InputLabel>
|
||||
<Select value={filtroIdDistribuidor} label="Distribuidor" onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
{distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
||||
<InputLabel>Empresa (Saldo)</InputLabel>
|
||||
<Select value={filtroIdEmpresa} label="Empresa (Saldo)" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}>
|
||||
<MenuItem value=""><em>Todas</em></MenuItem>
|
||||
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 150, flexGrow: 1 }}>
|
||||
<InputLabel>Tipo Mov.</InputLabel>
|
||||
<Select value={filtroTipoMov} label="Tipo Mov." onChange={(e) => setFiltroTipoMov(e.target.value as 'Recibido' | 'Realizado' | '')}>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
<MenuItem value="Recibido">Recibido</MenuItem>
|
||||
<MenuItem value="Realizado">Realizado</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Pago</Button>)}
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
|
||||
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{pageApiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{pageApiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && puedeVer && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Fecha</TableCell><TableCell>Distribuidor</TableCell><TableCell>Empresa (Saldo)</TableCell>
|
||||
<TableCell>Tipo Mov.</TableCell><TableCell>Recibo N°</TableCell>
|
||||
<TableCell align="right">Monto</TableCell><TableCell>Tipo Pago</TableCell>
|
||||
<TableCell>Detalle</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 9 : 8} align="center">No se encontraron pagos.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((p) => (
|
||||
<TableRow key={p.idPago} hover>
|
||||
<TableCell>{formatDate(p.fecha)}</TableCell><TableCell>{p.nombreDistribuidor}</TableCell>
|
||||
<TableCell>{p.nombreEmpresa}</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={p.tipoMovimiento} color={p.tipoMovimiento === 'Recibido' ? 'success' : 'warning'} size="small"/>
|
||||
</TableCell>
|
||||
<TableCell>{p.recibo}</TableCell>
|
||||
<TableCell align="right">${p.monto.toFixed(2)}</TableCell>
|
||||
<TableCell>{p.nombreTipoPago}</TableCell>
|
||||
<TableCell><Tooltip title={p.detalle || ''}><Box sx={{maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{p.detalle || '-'}</Box></Tooltip></TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={pagos.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Fecha</TableCell><TableCell>Distribuidor</TableCell><TableCell>Empresa (Saldo)</TableCell>
|
||||
<TableCell>Tipo Mov.</TableCell><TableCell>Recibo N°</TableCell>
|
||||
<TableCell align="right">Monto</TableCell><TableCell>Tipo Pago</TableCell>
|
||||
<TableCell>Detalle</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 9 : 8} align="center">No se encontraron pagos.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((p) => (
|
||||
<TableRow key={p.idPago} hover>
|
||||
<TableCell>{formatDate(p.fecha)}</TableCell><TableCell>{p.nombreDistribuidor}</TableCell>
|
||||
<TableCell>{p.nombreEmpresa}</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={p.tipoMovimiento} color={p.tipoMovimiento === 'Recibido' ? 'success' : 'warning'} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>{p.recibo}</TableCell>
|
||||
<TableCell align="right">${p.monto.toFixed(2)}</TableCell>
|
||||
<TableCell>{p.nombreTipoPago}</TableCell>
|
||||
<TableCell><Tooltip title={p.detalle || ''}><Box sx={{ maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.detalle || '-'}</Box></Tooltip></TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={pagos.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && selectedRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)}
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
||||
{puedeEliminar && selectedRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedRow.idPago)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
|
||||
<MenuItem onClick={() => handleDelete(selectedRow.idPago)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)}
|
||||
</Menu>
|
||||
|
||||
<PagoDistribuidorFormModal
|
||||
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
|
||||
initialData={editingPago} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
open={modalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSubmit={handleSubmitModal}
|
||||
initialData={editingPago}
|
||||
errorMessage={modalApiErrorMessage}
|
||||
clearErrorMessage={clearModalApiErrorMessage}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// src/pages/Distribucion/GestionarDistribuidoresPage.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem,
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Switch,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert
|
||||
CircularProgress, Alert, Chip, FormControlLabel, ListItemIcon, ListItemText
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import TrashIcon from '@mui/icons-material/Delete';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
||||
import ToggleOffIcon from '@mui/icons-material/ToggleOff';
|
||||
import distribuidorService from '../../services/Distribucion/distribuidorService';
|
||||
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
|
||||
import type { CreateDistribuidorDto } from '../../models/dtos/Distribucion/CreateDistribuidorDto';
|
||||
@@ -24,6 +26,7 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
const [filtroNroDoc, setFiltroNroDoc] = useState('');
|
||||
const [filtroSoloActivos, setFiltroSoloActivos] = useState<boolean | undefined>(true);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingDistribuidor, setEditingDistribuidor] = useState<DistribuidorDto | null>(null);
|
||||
@@ -49,12 +52,12 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
const data = await distribuidorService.getAllDistribuidores(filtroNombre, filtroNroDoc);
|
||||
const data = await distribuidorService.getAllDistribuidores(filtroNombre, filtroNroDoc, filtroSoloActivos);
|
||||
setDistribuidores(data);
|
||||
} catch (err) {
|
||||
console.error(err); setError('Error al cargar los distribuidores.');
|
||||
} finally { setLoading(false); }
|
||||
}, [filtroNombre, filtroNroDoc, puedeVer]);
|
||||
}, [filtroNombre, filtroNroDoc, filtroSoloActivos, puedeVer]);
|
||||
|
||||
useEffect(() => { cargarDistribuidores(); }, [cargarDistribuidores]);
|
||||
|
||||
@@ -94,6 +97,21 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleToggleBaja = async (distribuidor: DistribuidorDto) => {
|
||||
setApiErrorMessage(null);
|
||||
const accion = distribuidor.baja ? "reactivar" : "dar de baja";
|
||||
if (window.confirm(`¿Está seguro de que desea ${accion} a ${distribuidor.nombre}?`)) {
|
||||
try {
|
||||
await distribuidorService.toggleBajaDistribuidor(distribuidor.idDistribuidor, { darDeBaja: !distribuidor.baja, fechaBaja: !distribuidor.baja ? new Date().toISOString() : null });
|
||||
cargarDistribuidores();
|
||||
} catch (err: any) {
|
||||
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : `Error al ${accion} el distribuidor.`;
|
||||
setApiErrorMessage(message);
|
||||
}
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, distribuidor: DistribuidorDto) => {
|
||||
setAnchorEl(event.currentTarget); setSelectedDistribuidorRow(distribuidor);
|
||||
};
|
||||
@@ -132,7 +150,17 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
onChange={(e) => setFiltroNroDoc(e.target.value)}
|
||||
sx={{ flexGrow: 1, minWidth: '200px' }}
|
||||
/>
|
||||
{/* <Button variant="contained" onClick={cargarDistribuidores} size="small">Buscar</Button> */}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={filtroSoloActivos === undefined ? true : filtroSoloActivos}
|
||||
onChange={(e) => setFiltroSoloActivos(e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label="Ver Activos"
|
||||
sx={{ flexShrink: 0 }}
|
||||
/>
|
||||
</Box>
|
||||
{puedeCrear && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Distribuidor</Button>
|
||||
@@ -150,6 +178,7 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
<TableCell>Nombre</TableCell><TableCell>Nro. Doc.</TableCell>
|
||||
<TableCell>Contacto</TableCell><TableCell>Zona</TableCell>
|
||||
<TableCell>Teléfono</TableCell><TableCell>Localidad</TableCell>
|
||||
<TableCell>Estado</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
@@ -157,10 +186,11 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
<TableRow><TableCell colSpan={7} align="center">No se encontraron distribuidores.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((d) => (
|
||||
<TableRow key={d.idDistribuidor} hover>
|
||||
<TableRow key={d.idDistribuidor} hover sx={{ backgroundColor: d.baja ? '#ffebee' : 'inherit' }}>
|
||||
<TableCell>{d.nombre}</TableCell><TableCell>{d.nroDoc}</TableCell>
|
||||
<TableCell>{d.contacto || '-'}</TableCell><TableCell>{d.nombreZona || '-'}</TableCell>
|
||||
<TableCell>{d.telefono || '-'}</TableCell><TableCell>{d.localidad || '-'}</TableCell>
|
||||
<TableCell>{d.baja ? <Chip label="Baja" color="error" size="small" /> : <Chip label="Activo" color="success" size="small" />}</TableCell>
|
||||
{(puedeModificar || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, d)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
|
||||
@@ -179,8 +209,24 @@ const GestionarDistribuidoresPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} />Modificar</MenuItem>)}
|
||||
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}><TrashIcon fontSize="small" sx={{ mr: 1 }} />Eliminar</MenuItem>)}
|
||||
{puedeModificar && selectedDistribuidorRow && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow); handleMenuClose(); }}>
|
||||
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Modificar</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && selectedDistribuidorRow && (
|
||||
<MenuItem onClick={() => handleToggleBaja(selectedDistribuidorRow)}>
|
||||
<ListItemIcon>{selectedDistribuidorRow.baja ? <ToggleOnIcon fontSize="small" /> : <ToggleOffIcon fontSize="small" />}</ListItemIcon>
|
||||
<ListItemText>{selectedDistribuidorRow.baja ? 'Reactivar' : 'Dar de Baja'}</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{puedeEliminar && selectedDistribuidorRow && (
|
||||
<MenuItem onClick={() => handleDelete(selectedDistribuidorRow.idDistribuidor)}>
|
||||
<ListItemIcon><TrashIcon fontSize="small" /></ListItemIcon>
|
||||
<ListItemText>Eliminar (Físico)</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
|
||||
</Menu>
|
||||
|
||||
|
||||
@@ -135,9 +135,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
if (!filtroFecha || !filtroIdCanillitaSeleccionado) {
|
||||
if (loading) setLoading(false);
|
||||
setMovimientos([]);
|
||||
return;
|
||||
if (loading) setLoading(false);
|
||||
setMovimientos([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true); setError(null); setApiErrorMessage(null);
|
||||
try {
|
||||
@@ -164,37 +164,37 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (filtroFecha && filtroIdCanillitaSeleccionado) {
|
||||
cargarMovimientos();
|
||||
cargarMovimientos();
|
||||
} else {
|
||||
setMovimientos([]);
|
||||
if (loading) setLoading(false);
|
||||
setMovimientos([]);
|
||||
if (loading) setLoading(false);
|
||||
}
|
||||
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]);
|
||||
|
||||
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
|
||||
if (!puedeCrear && !item) {
|
||||
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
|
||||
return;
|
||||
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
|
||||
return;
|
||||
}
|
||||
if (item && !puedeModificar) {
|
||||
setApiErrorMessage("No tiene permiso para modificar movimientos.");
|
||||
return;
|
||||
setApiErrorMessage("No tiene permiso para modificar movimientos.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (item) {
|
||||
setEditingMovimiento(item);
|
||||
setPrefillModalData(null);
|
||||
setEditingMovimiento(item);
|
||||
setPrefillModalData(null);
|
||||
} else {
|
||||
const canillitaSeleccionado = destinatariosDropdown.find(
|
||||
c => c.idCanilla === Number(filtroIdCanillitaSeleccionado)
|
||||
);
|
||||
setEditingMovimiento(null);
|
||||
setPrefillModalData({
|
||||
fecha: filtroFecha,
|
||||
idCanilla: filtroIdCanillitaSeleccionado,
|
||||
nombreCanilla: canillitaSeleccionado?.nomApe,
|
||||
idPublicacion: filtroIdPublicacion
|
||||
});
|
||||
const canillitaSeleccionado = destinatariosDropdown.find(
|
||||
c => c.idCanilla === Number(filtroIdCanillitaSeleccionado)
|
||||
);
|
||||
setEditingMovimiento(null);
|
||||
setPrefillModalData({
|
||||
fecha: filtroFecha,
|
||||
idCanilla: filtroIdCanillitaSeleccionado,
|
||||
nombreCanilla: canillitaSeleccionado?.nomApe,
|
||||
idPublicacion: filtroIdPublicacion
|
||||
});
|
||||
}
|
||||
setApiErrorMessage(null);
|
||||
setModalOpen(true);
|
||||
@@ -224,7 +224,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
});
|
||||
};
|
||||
const handleSelectAllForLiquidar = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.checked) {
|
||||
if (event.target.checked) {
|
||||
const newSelectedIds = new Set(movimientos.filter(m => !m.liquidado).map(m => m.idParte));
|
||||
setSelectedIdsParaLiquidar(newSelectedIds);
|
||||
} else {
|
||||
@@ -248,17 +248,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z');
|
||||
let fechaMovimientoMasReciente: Date | null = null;
|
||||
selectedIdsParaLiquidar.forEach(idParte => {
|
||||
const movimiento = movimientos.find(m => m.idParte === idParte);
|
||||
if (movimiento && movimiento.fecha) {
|
||||
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z');
|
||||
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
fechaMovimientoMasReciente = movFecha;
|
||||
}
|
||||
const movimiento = movimientos.find(m => m.idParte === idParte);
|
||||
if (movimiento && movimiento.fecha) {
|
||||
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z');
|
||||
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
fechaMovimientoMasReciente = movFecha;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) {
|
||||
setApiErrorMessage(`La fecha de liquidación (${fechaLiquidacionDate.toLocaleDateString('es-AR', {timeZone: 'UTC'})}) no puede ser inferior a la fecha del movimiento más reciente a liquidar (${(fechaMovimientoMasReciente as Date).toLocaleDateString('es-AR', {timeZone: 'UTC'})}).`);
|
||||
return;
|
||||
setApiErrorMessage(`La fecha de liquidación (${fechaLiquidacionDate.toLocaleDateString('es-AR', { timeZone: 'UTC' })}) no puede ser inferior a la fecha del movimiento más reciente a liquidar (${(fechaMovimientoMasReciente as Date).toLocaleDateString('es-AR', { timeZone: 'UTC' })}).`);
|
||||
return;
|
||||
}
|
||||
setApiErrorMessage(null);
|
||||
setLoading(true);
|
||||
@@ -277,9 +277,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) {
|
||||
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
|
||||
await handleImprimirTicketLiquidacion(
|
||||
movimientoParaTicket.idCanilla,
|
||||
movimientoParaTicket.fecha,
|
||||
false
|
||||
movimientoParaTicket.idCanilla,
|
||||
movimientoParaTicket.fecha,
|
||||
false
|
||||
);
|
||||
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
|
||||
console.log("Liquidación exitosa para accionista. No se genera ticket automáticamente.");
|
||||
@@ -346,7 +346,11 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
|
||||
const totalARendirVisible = useMemo(() =>
|
||||
displayData.filter(m => !m.liquidado).reduce((sum, item) => sum + item.montoARendir, 0)
|
||||
, [displayData]);
|
||||
, [displayData]);
|
||||
|
||||
const montoARendirAll = useMemo(() =>
|
||||
movimientos.reduce((sum, item) => sum + item.montoARendir, 0)
|
||||
, [movimientos]);
|
||||
|
||||
if (!puedeVer) {
|
||||
return (
|
||||
@@ -367,7 +371,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
|
||||
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
|
||||
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
|
||||
onChange={(e) => setFiltroFecha(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }}
|
||||
required
|
||||
@@ -398,9 +402,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
onChange={(e) => setFiltroIdCanillitaSeleccionado(e.target.value as number | string)}
|
||||
>
|
||||
<MenuItem value=""><em>Seleccione uno</em></MenuItem>
|
||||
{destinatariosDropdown.map(c => <MenuItem key={c.idCanilla} value={c.idCanilla}>{c.nomApe} {c.legajo ? `(Leg: ${c.legajo})`: ''}</MenuItem>)}
|
||||
{destinatariosDropdown.map(c => <MenuItem key={c.idCanilla} value={c.idCanilla}>{c.nomApe} {c.legajo ? `(Leg: ${c.legajo})` : ''}</MenuItem>)}
|
||||
</Select>
|
||||
{!filtroIdCanillitaSeleccionado && <Typography component="p" color="error" variant="caption" sx={{ml:1.5, fontSize:'0.65rem'}}>Selección obligatoria</Typography>}
|
||||
{!filtroIdCanillitaSeleccionado && <Typography component="p" color="error" variant="caption" sx={{ ml: 1.5, fontSize: '0.65rem' }}>Selección obligatoria</Typography>}
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
|
||||
@@ -411,15 +415,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap:2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 2 }}>
|
||||
{puedeCrear && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => handleOpenModal()}
|
||||
disabled={!filtroFecha || !filtroIdCanillitaSeleccionado}
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => handleOpenModal()}
|
||||
disabled={!filtroFecha || !filtroIdCanillitaSeleccionado}
|
||||
>
|
||||
Registrar Movimiento
|
||||
Registrar Movimiento
|
||||
</Button>
|
||||
)}
|
||||
{puedeLiquidar && numSelectedToLiquidate > 0 && movimientos.some(m => selectedIdsParaLiquidar.has(m.idParte) && !m.liquidado) && (
|
||||
@@ -430,31 +434,38 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{!filtroFecha && <Alert severity="info" sx={{my:1}}>Por favor, seleccione una fecha.</Alert>}
|
||||
{filtroFecha && !filtroIdCanillitaSeleccionado && <Alert severity="info" sx={{my:1}}>Por favor, seleccione un {filtroTipoDestinatario === 'canillitas' ? 'canillita' : 'accionista'}.</Alert>}
|
||||
{!filtroFecha && <Alert severity="info" sx={{ my: 1 }}>Por favor, seleccione una fecha.</Alert>}
|
||||
{filtroFecha && !filtroIdCanillitaSeleccionado && <Alert severity="info" sx={{ my: 1 }}>Por favor, seleccione un {filtroTipoDestinatario === 'canillitas' ? 'canillita' : 'accionista'}.</Alert>}
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
{loadingTicketPdf && ( <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}> <CircularProgress size={20} sx={{ mr: 1 }} /> <Typography variant="body2">Cargando ticket...</Typography> </Box> )}
|
||||
{loadingTicketPdf && (<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}> <CircularProgress size={20} sx={{ mr: 1 }} /> <Typography variant="body2">Cargando ticket...</Typography> </Box>)}
|
||||
|
||||
|
||||
{!loading && movimientos.length > 0 && (
|
||||
<Paper sx={{ p: 1.5, mb: 2, mt:1, backgroundColor: 'grey.100' }}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}>
|
||||
<Typography variant="subtitle1" sx={{mr:2}}>
|
||||
Total a Liquidar:
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{fontWeight: 'bold', color: 'error.main'}}>
|
||||
{totalARendirVisible.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Paper sx={{ p: 1.5, mb: 2, mt: 1, backgroundColor: 'grey.100' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
|
||||
<Typography variant="subtitle1" sx={{ mr: 1 }}>
|
||||
Total:
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ paddingRight: '5px', fontWeight: 'bold', color: 'text.main' }}>
|
||||
{montoARendirAll.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</Typography>
|
||||
-
|
||||
<Typography variant="subtitle1" sx={{ mr: 1, paddingLeft: '5px', }}>
|
||||
Total a Liquidar:
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', color: totalARendirVisible > 0 ? 'error.main' : 'green' }}>
|
||||
{totalARendirVisible.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{!loading && !error && puedeVer && filtroFecha && filtroIdCanillitaSeleccionado && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{puedeLiquidar && (
|
||||
@@ -545,7 +556,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{puedeModificar && selectedRow && !selectedRow.liquidado && (
|
||||
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
|
||||
|
||||
@@ -560,7 +571,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
selectedRow.canillaEsAccionista
|
||||
);
|
||||
}
|
||||
handleMenuClose();
|
||||
handleMenuClose();
|
||||
}}
|
||||
disabled={loadingTicketPdf}
|
||||
>
|
||||
@@ -572,9 +583,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
{selectedRow && (
|
||||
((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados))
|
||||
) && (
|
||||
<MenuItem onClick={() => {if (selectedRow) handleDelete(selectedRow.idParte);}}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
Eliminar
|
||||
<MenuItem onClick={() => { if (selectedRow) handleDelete(selectedRow.idParte); }}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
Eliminar
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
@@ -589,7 +600,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
|
||||
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
|
||||
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
|
||||
<DialogTitle>Confirmar Liquidación</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
||||
CircularProgress, Alert, FormControl, InputLabel, Select, FormControlLabel, Checkbox
|
||||
Alert, FormControl, InputLabel, Select, FormControlLabel, Checkbox
|
||||
} from '@mui/material';
|
||||
import { DataGrid, type GridColDef, type GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { esES } from '@mui/x-data-grid/locales';
|
||||
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
@@ -11,6 +13,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import EditCalendarIcon from '@mui/icons-material/EditCalendar';
|
||||
|
||||
import stockBobinaService from '../../services/Impresion/stockBobinaService';
|
||||
import tipoBobinaService from '../../services/Impresion/tipoBobinaService';
|
||||
@@ -24,10 +27,13 @@ import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/Cambiar
|
||||
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
|
||||
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
|
||||
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
|
||||
import type { UpdateFechaRemitoLoteDto } from '../../models/dtos/Impresion/UpdateFechaRemitoLoteDto';
|
||||
|
||||
import StockBobinaFechaRemitoModal from '../../components/Modals/Impresion/StockBobinaFechaRemitoModal';
|
||||
import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
|
||||
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
|
||||
import StockBobinaCambioEstadoModal from '../../components/Modals/Impresion/StockBobinaCambioEstadoModal';
|
||||
import StockBobinaLoteFormModal from '../../components/Modals/Impresion/StockBobinaLoteFormModal';
|
||||
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
@@ -38,20 +44,27 @@ const ID_ESTADO_DANADA = 3;
|
||||
|
||||
const GestionarStockBobinasPage: React.FC = () => {
|
||||
const [stock, setStock] = useState<StockBobinaDto[]>([]);
|
||||
const [loading, setLoading] = useState(false); // No carga al inicio
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Estados de los filtros
|
||||
// --- Estados de los filtros ---
|
||||
const [filtroTipoBobina, setFiltroTipoBobina] = useState<number | string>('');
|
||||
const [filtroNroBobina, setFiltroNroBobina] = useState('');
|
||||
const [filtroPlanta, setFiltroPlanta] = useState<number | string>('');
|
||||
const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>('');
|
||||
const [filtroRemito, setFiltroRemito] = useState('');
|
||||
const [filtroFechaHabilitado, setFiltroFechaHabilitado] = useState<boolean>(false); // <-- NUEVO
|
||||
|
||||
// Filtro Fechas Remito
|
||||
const [filtroFechaHabilitado, setFiltroFechaHabilitado] = useState<boolean>(false);
|
||||
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
|
||||
// Nuevo Filtro: Fechas Estado
|
||||
const [filtroFechaEstadoHabilitado, setFiltroFechaEstadoHabilitado] = useState<boolean>(false);
|
||||
const [filtroFechaEstadoDesde, setFiltroFechaEstadoDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [filtroFechaEstadoHasta, setFiltroFechaEstadoHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
|
||||
// Estados para datos de dropdowns
|
||||
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
|
||||
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
|
||||
@@ -62,13 +75,10 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = useState(false);
|
||||
const [loteModalOpen, setLoteModalOpen] = useState(false);
|
||||
const [fechaRemitoModalOpen, setFechaRemitoModalOpen] = useState(false);
|
||||
|
||||
// Estado para la bobina seleccionada en un modal o menú
|
||||
const [selectedBobina, setSelectedBobina] = useState<StockBobinaDto | null>(null);
|
||||
|
||||
// Estados para la paginación y el menú de acciones
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(25);
|
||||
// Menú de acciones
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedBobinaForRowMenu, setSelectedBobinaForRowMenu] = useState<StockBobinaDto | null>(null);
|
||||
|
||||
@@ -79,12 +89,9 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
const puedeModificarDatos = isSuperAdmin || tienePermiso("IB004");
|
||||
const puedeEliminar = isSuperAdmin || tienePermiso("IB005");
|
||||
|
||||
const lastOpenedMenuButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const fetchFiltersDropdownData = useCallback(async () => {
|
||||
setLoadingFiltersDropdown(true);
|
||||
try {
|
||||
// Asumiendo que estos servicios existen y devuelven los DTOs correctos
|
||||
const [tiposData, plantasData, estadosData] = await Promise.all([
|
||||
tipoBobinaService.getAllTiposBobina(),
|
||||
plantaService.getAllPlantas(),
|
||||
@@ -121,13 +128,18 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
idPlanta: filtroPlanta ? Number(filtroPlanta) : null,
|
||||
idEstadoBobina: filtroEstadoBobina ? Number(filtroEstadoBobina) : null,
|
||||
remitoFilter: filtroRemito || null,
|
||||
// Fechas Remito
|
||||
fechaDesde: filtroFechaHabilitado ? filtroFechaDesde : null,
|
||||
fechaHasta: filtroFechaHabilitado ? filtroFechaHasta : null,
|
||||
// Fechas Estado (Nuevos parametros, asegurar que el backend los reciba)
|
||||
fechaEstadoDesde: filtroFechaEstadoHabilitado ? filtroFechaEstadoDesde : null,
|
||||
fechaEstadoHasta: filtroFechaEstadoHabilitado ? filtroFechaEstadoHasta : null,
|
||||
};
|
||||
const data = await stockBobinaService.getAllStockBobinas(params);
|
||||
setStock(data);
|
||||
if (data.length === 0) {
|
||||
setError("No se encontraron resultados con los filtros aplicados.");
|
||||
// No setteamos error bloqueante, solo aviso visual si se desea, o dejar tabla vacía.
|
||||
// setError("No se encontraron resultados con los filtros aplicados.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -135,10 +147,14 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaHabilitado, filtroFechaDesde, filtroFechaHasta]);
|
||||
}, [
|
||||
puedeVer,
|
||||
filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito,
|
||||
filtroFechaHabilitado, filtroFechaDesde, filtroFechaHasta,
|
||||
filtroFechaEstadoHabilitado, filtroFechaEstadoDesde, filtroFechaEstadoHasta
|
||||
]);
|
||||
|
||||
const handleBuscarClick = () => {
|
||||
setPage(0); // Resetear la paginación al buscar
|
||||
cargarStock();
|
||||
};
|
||||
|
||||
@@ -148,14 +164,19 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
setFiltroPlanta('');
|
||||
setFiltroEstadoBobina('');
|
||||
setFiltroRemito('');
|
||||
|
||||
setFiltroFechaHabilitado(false);
|
||||
setFiltroFechaDesde(new Date().toISOString().split('T')[0]);
|
||||
setFiltroFechaHasta(new Date().toISOString().split('T')[0]);
|
||||
setStock([]); // Limpiar los resultados actuales
|
||||
|
||||
setFiltroFechaEstadoHabilitado(false);
|
||||
setFiltroFechaEstadoDesde(new Date().toISOString().split('T')[0]);
|
||||
setFiltroFechaEstadoHasta(new Date().toISOString().split('T')[0]);
|
||||
|
||||
setStock([]);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); };
|
||||
const handleCloseIngresoModal = () => setIngresoModalOpen(false);
|
||||
const handleSubmitIngresoModal = async (data: CreateStockBobinaDto) => {
|
||||
setApiErrorMessage(null);
|
||||
@@ -163,92 +184,166 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al ingresar bobina.'; setApiErrorMessage(msg); throw err; }
|
||||
};
|
||||
|
||||
const handleOpenEditModal = (bobina: StockBobinaDto | null) => {
|
||||
if (!bobina) return;
|
||||
setSelectedBobina(bobina);
|
||||
setApiErrorMessage(null);
|
||||
setEditModalOpen(true);
|
||||
};
|
||||
const handleCloseEditModal = () => {
|
||||
setEditModalOpen(false);
|
||||
setSelectedBobina(null);
|
||||
if (lastOpenedMenuButtonRef.current) {
|
||||
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
|
||||
const handleLoteModalClose = (refrescar: boolean) => {
|
||||
setLoteModalOpen(false);
|
||||
if (refrescar) {
|
||||
cargarStock();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => {
|
||||
setApiErrorMessage(null);
|
||||
try { await stockBobinaService.updateDatosBobinaDisponible(idBobina, data); cargarStock(); }
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al actualizar bobina.'; setApiErrorMessage(msg); throw err; }
|
||||
};
|
||||
|
||||
const handleOpenCambioEstadoModal = (bobina: StockBobinaDto | null) => {
|
||||
if (!bobina) return;
|
||||
setSelectedBobina(bobina);
|
||||
setApiErrorMessage(null);
|
||||
setCambioEstadoModalOpen(true);
|
||||
};
|
||||
const handleCloseCambioEstadoModal = () => setCambioEstadoModalOpen(false);
|
||||
const handleSubmitCambioEstadoModal = async (idBobina: number, data: CambiarEstadoBobinaDto) => {
|
||||
setApiErrorMessage(null);
|
||||
try { await stockBobinaService.cambiarEstadoBobina(idBobina, data); cargarStock(); }
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar estado.'; setApiErrorMessage(msg); throw err; }
|
||||
};
|
||||
|
||||
const handleDeleteBobina = async (bobina: StockBobinaDto | null) => {
|
||||
if (!bobina) return;
|
||||
if (bobina.idEstadoBobina !== ID_ESTADO_DISPONIBLE && bobina.idEstadoBobina !== ID_ESTADO_DANADA) {
|
||||
const handleDeleteBobina = () => {
|
||||
if (!selectedBobinaForRowMenu) return;
|
||||
|
||||
if (selectedBobinaForRowMenu.idEstadoBobina !== ID_ESTADO_DISPONIBLE && selectedBobinaForRowMenu.idEstadoBobina !== ID_ESTADO_DANADA) {
|
||||
alert("Solo se pueden eliminar bobinas en estado 'Disponible' o 'Dañada'.");
|
||||
handleMenuClose();
|
||||
return;
|
||||
}
|
||||
if (window.confirm(`¿Seguro de eliminar este ingreso de bobina (ID: ${bobina.idBobina})?`)) {
|
||||
if (window.confirm(`¿Seguro de eliminar este ingreso de bobina (ID: ${selectedBobinaForRowMenu.idBobina})?`)) {
|
||||
setApiErrorMessage(null);
|
||||
try { await stockBobinaService.deleteIngresoBobina(bobina.idBobina); cargarStock(); }
|
||||
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
|
||||
stockBobinaService.deleteIngresoBobina(selectedBobinaForRowMenu.idBobina)
|
||||
.then(() => cargarStock())
|
||||
.catch((err: any) => {
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
|
||||
setApiErrorMessage(msg);
|
||||
});
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>, bobina: StockBobinaDto) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedBobinaForRowMenu(bobina);
|
||||
lastOpenedMenuButtonRef.current = event.currentTarget;
|
||||
};
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedBobinaForRowMenu(null);
|
||||
if (lastOpenedMenuButtonRef.current) {
|
||||
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
|
||||
const handleSubmitFechaRemitoModal = async (data: UpdateFechaRemitoLoteDto) => {
|
||||
setApiErrorMessage(null);
|
||||
try {
|
||||
await stockBobinaService.actualizarFechaRemitoLote(data);
|
||||
cargarStock();
|
||||
} catch (err: any) {
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al actualizar la fecha del remito.';
|
||||
setApiErrorMessage(msg);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
|
||||
// --- Handlers Menú Acciones ---
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>, bobina: StockBobinaDto) => {
|
||||
event.stopPropagation(); // Evitar selección de fila al abrir menú
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedBobinaForRowMenu(bobina);
|
||||
};
|
||||
const displayData = stock.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
const formatDate = (dateString?: string | null) => {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
timeZone: 'UTC'
|
||||
};
|
||||
return new Intl.DateTimeFormat('es-AR', options).format(date);
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleOpenEditModal = () => { setEditModalOpen(true); handleMenuClose(); };
|
||||
const handleOpenCambioEstadoModal = () => { setCambioEstadoModalOpen(true); handleMenuClose(); };
|
||||
const handleOpenFechaRemitoModal = () => { setFechaRemitoModalOpen(true); handleMenuClose(); };
|
||||
|
||||
const handleCloseEditModal = () => { setEditModalOpen(false); setSelectedBobinaForRowMenu(null); };
|
||||
const handleCloseCambioEstadoModal = () => { setCambioEstadoModalOpen(false); setSelectedBobinaForRowMenu(null); };
|
||||
const handleCloseFechaRemitoModal = () => { setFechaRemitoModalOpen(false); setSelectedBobinaForRowMenu(null); };
|
||||
|
||||
// --- Definición de Columnas DataGrid ---
|
||||
const columns = useMemo<GridColDef<StockBobinaDto>[]>(() => [
|
||||
{ field: 'nroBobina', headerName: 'Nro. Bobina', width: 130 },
|
||||
{ field: 'nombreTipoBobina', headerName: 'Tipo', width: 200, flex: 1 },
|
||||
{ field: 'peso', headerName: 'Peso (Kg)', width: 100, align: 'right', headerAlign: 'right', type: 'number' },
|
||||
{ field: 'nombrePlanta', headerName: 'Planta', width: 120 },
|
||||
{
|
||||
field: 'nombreEstadoBobina',
|
||||
headerName: 'Estado',
|
||||
width: 130,
|
||||
renderCell: (params) => {
|
||||
const idEstado = params.row.idEstadoBobina;
|
||||
let color: "success" | "primary" | "error" | "default" = "default";
|
||||
if (idEstado === ID_ESTADO_DISPONIBLE) color = "success";
|
||||
else if (idEstado === ID_ESTADO_UTILIZADA) color = "primary";
|
||||
else if (idEstado === ID_ESTADO_DANADA) color = "error";
|
||||
|
||||
return <Chip label={params.value} size="small" color={color} variant="outlined" />;
|
||||
}
|
||||
},
|
||||
{ field: 'remito', headerName: 'Remito', width: 120 },
|
||||
{
|
||||
field: 'fechaRemito',
|
||||
headerName: 'F. Remito',
|
||||
width: 110,
|
||||
type: 'date',
|
||||
valueGetter: (value: string) => {
|
||||
if (!value) return null;
|
||||
const datePart = value.toString().split('T')[0];
|
||||
const [year, month, day] = datePart.split('-');
|
||||
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||
},
|
||||
valueFormatter: (value: Date) => {
|
||||
return value ? value.toLocaleDateString('es-AR') : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'fechaEstado',
|
||||
headerName: 'F. Estado',
|
||||
width: 110,
|
||||
type: 'date',
|
||||
valueGetter: (value: string) => {
|
||||
if (!value) return null;
|
||||
const datePart = value.toString().split('T')[0];
|
||||
const [year, month, day] = datePart.split('-');
|
||||
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||
},
|
||||
valueFormatter: (value: Date) => {
|
||||
return value ? value.toLocaleDateString('es-AR') : '-';
|
||||
}
|
||||
},
|
||||
|
||||
{ field: 'nombrePublicacion', headerName: 'Publicación', width: 150 },
|
||||
{ field: 'nombreSeccion', headerName: 'Sección', width: 120 },
|
||||
{ field: 'obs', headerName: 'Obs.', width: 200, flex: 1 },
|
||||
{
|
||||
field: 'acciones',
|
||||
headerName: 'Acciones',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
align: 'right',
|
||||
renderCell: (params: GridRenderCellParams<StockBobinaDto>) => {
|
||||
const b = params.row;
|
||||
const disabled = !(puedeModificarDatos) &&
|
||||
!(puedeCambiarEstado) &&
|
||||
!((b.idEstadoBobina === ID_ESTADO_DISPONIBLE || b.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar);
|
||||
|
||||
if (disabled) return null;
|
||||
|
||||
return (
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, b)} size="small">
|
||||
<MoreVertIcon fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
], [puedeModificarDatos, puedeCambiarEstado, puedeEliminar]);
|
||||
|
||||
if (!puedeVer) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h5" gutterBottom>Stock de Bobinas</Typography>
|
||||
|
||||
{/* Panel de Filtros */}
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Filtros</Typography>
|
||||
|
||||
{/* Fila 1: Filtros generales */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
|
||||
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
|
||||
<InputLabel>Tipo Bobina</InputLabel>
|
||||
@@ -274,128 +369,123 @@ const GestionarStockBobinasPage: React.FC = () => {
|
||||
</FormControl>
|
||||
<TextField label="Remito" size="small" value={filtroRemito} onChange={(e) => setFiltroRemito(e.target.value)} sx={{ minWidth: 150, flexGrow: 1 }} />
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={filtroFechaHabilitado} onChange={(e) => setFiltroFechaHabilitado(e.target.checked)} />}
|
||||
label="Filtrar por Fechas de Remitos"
|
||||
/>
|
||||
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} disabled={!filtroFechaHabilitado} />
|
||||
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} disabled={!filtroFechaHabilitado} />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
mb: 2,
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
>
|
||||
<Button variant="contained" startIcon={<SearchIcon />} onClick={handleBuscarClick} disabled={loading}>Buscar</Button>
|
||||
<Button variant="outlined" startIcon={<ClearIcon />} onClick={handleLimpiarFiltros} disabled={loading}>Limpiar Filtros</Button>
|
||||
</Box>
|
||||
{puedeIngresar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenIngresoModal} sx={{ ml: 'auto' }}>Ingresar Bobina</Button>)}
|
||||
|
||||
{/* Fila 2: Filtros de Fechas */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 4, mb: 2, alignItems: 'center' }}>
|
||||
{/* Fechas Remito */}
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', border: '1px dashed #ccc', p: 1, borderRadius: 1 }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={filtroFechaHabilitado} onChange={(e) => setFiltroFechaHabilitado(e.target.checked)} />}
|
||||
label="Filtrar por Fecha Remito"
|
||||
/>
|
||||
<TextField label="Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 140 }} disabled={!filtroFechaHabilitado} />
|
||||
<TextField label="Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 140 }} disabled={!filtroFechaHabilitado} />
|
||||
</Box>
|
||||
|
||||
{/* Fechas Estado (Nuevo) */}
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', border: '1px dashed #ccc', p: 1, borderRadius: 1 }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={filtroFechaEstadoHabilitado} onChange={(e) => setFiltroFechaEstadoHabilitado(e.target.checked)} />}
|
||||
label="Filtrar por Fecha Estado"
|
||||
/>
|
||||
<TextField label="Desde" type="date" size="small" value={filtroFechaEstadoDesde} onChange={(e) => setFiltroFechaEstadoDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 140 }} disabled={!filtroFechaEstadoHabilitado} />
|
||||
<TextField label="Hasta" type="date" size="small" value={filtroFechaEstadoHasta} onChange={(e) => setFiltroFechaEstadoHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 140 }} disabled={!filtroFechaEstadoHabilitado} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Botones de acción del filtro */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 2, mt: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button variant="contained" startIcon={<SearchIcon />} onClick={handleBuscarClick} disabled={loading}>
|
||||
Buscar
|
||||
</Button>
|
||||
<Button variant="outlined" startIcon={<ClearIcon />} onClick={handleLimpiarFiltros} disabled={loading}>
|
||||
Limpiar Filtros
|
||||
</Button>
|
||||
</Box>
|
||||
{puedeIngresar && (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button variant="contained" color="secondary" startIcon={<AddIcon />} onClick={() => setLoteModalOpen(true)}>
|
||||
Ingreso por Remito (Lote)
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="warning" sx={{ my: 2 }}>{error}</Alert>}
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
|
||||
|
||||
{!loading && !error && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Nro. Bobina</TableCell><TableCell>Tipo</TableCell><TableCell>Peso (Kg)</TableCell>
|
||||
<TableCell>Planta</TableCell><TableCell>Estado</TableCell><TableCell>Remito</TableCell>
|
||||
<TableCell>F. Remito</TableCell><TableCell>F. Estado</TableCell>
|
||||
<TableCell>Publicación</TableCell><TableCell>Sección</TableCell>
|
||||
<TableCell>Obs.</TableCell>
|
||||
{(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{displayData.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) ? 12 : 11} align="center">No se encontraron bobinas con los filtros aplicados. Haga clic en "Buscar" para iniciar una consulta.</TableCell></TableRow>
|
||||
) : (
|
||||
displayData.map((b) => (
|
||||
<TableRow key={b.idBobina} hover>
|
||||
<TableCell>{b.nroBobina}</TableCell><TableCell>{b.nombreTipoBobina}</TableCell>
|
||||
<TableCell align="right">{b.peso}</TableCell><TableCell>{b.nombrePlanta}</TableCell>
|
||||
<TableCell><Chip label={b.nombreEstadoBobina} size="small" color={
|
||||
b.idEstadoBobina === ID_ESTADO_DISPONIBLE ? "success" : b.idEstadoBobina === ID_ESTADO_UTILIZADA ? "primary" : b.idEstadoBobina === ID_ESTADO_DANADA ? "error" : "default"
|
||||
} /></TableCell>
|
||||
<TableCell>{b.remito}</TableCell><TableCell>{formatDate(b.fechaRemito)}</TableCell>
|
||||
<TableCell>{formatDate(b.fechaEstado)}</TableCell>
|
||||
<TableCell>{b.nombrePublicacion || '-'}</TableCell><TableCell>{b.nombreSeccion || '-'}</TableCell>
|
||||
<TableCell>{b.obs || '-'}</TableCell>
|
||||
{(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) && (
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, b)}
|
||||
disabled={
|
||||
!(b.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos) &&
|
||||
!(puedeCambiarEstado) &&
|
||||
!((b.idEstadoBobina === ID_ESTADO_DISPONIBLE || b.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar)
|
||||
}
|
||||
><MoreVertIcon /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[25, 50, 100]} component="div" count={stock.length}
|
||||
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
|
||||
/>
|
||||
</TableContainer>
|
||||
)}
|
||||
{/* Tabla DataGrid */}
|
||||
<Paper sx={{ width: '100%', height: 600 }}>
|
||||
<DataGrid
|
||||
rows={stock}
|
||||
columns={columns}
|
||||
getRowId={(row) => row.idBobina} // Importante: especificar el ID único
|
||||
loading={loading}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
disableRowSelectionOnClick
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 25 } },
|
||||
}}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
sx={{ border: 0 }}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* Menú Contextual de Fila */}
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
{selectedBobinaForRowMenu && puedeModificarDatos && (
|
||||
<MenuItem onClick={handleOpenFechaRemitoModal}>
|
||||
<EditCalendarIcon fontSize="small" sx={{ mr: 1 }} /> Corregir Fecha Remito
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedBobinaForRowMenu && puedeModificarDatos && selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && (
|
||||
<MenuItem onClick={() => { handleOpenEditModal(selectedBobinaForRowMenu); handleMenuClose(); }}>
|
||||
<EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos
|
||||
<MenuItem onClick={handleOpenEditModal}>
|
||||
<EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos Bobina
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedBobinaForRowMenu && puedeCambiarEstado && (
|
||||
<MenuItem onClick={() => { handleOpenCambioEstadoModal(selectedBobinaForRowMenu); handleMenuClose(); }}>
|
||||
<MenuItem onClick={handleOpenCambioEstadoModal}>
|
||||
<SwapHorizIcon fontSize="small" sx={{ mr: 1 }} /> Cambiar Estado
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedBobinaForRowMenu && puedeEliminar &&
|
||||
(selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && (
|
||||
<MenuItem onClick={() => handleDeleteBobina(selectedBobinaForRowMenu)}>
|
||||
<MenuItem onClick={handleDeleteBobina}>
|
||||
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar Ingreso
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedBobinaForRowMenu &&
|
||||
!((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos)) &&
|
||||
!(puedeCambiarEstado) &&
|
||||
!(((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar)) &&
|
||||
<MenuItem disabled>Sin acciones disponibles</MenuItem>
|
||||
}
|
||||
</Menu>
|
||||
|
||||
{/* Modales sin cambios */}
|
||||
{/* Modales */}
|
||||
<StockBobinaIngresoFormModal
|
||||
open={ingresoModalOpen} onClose={handleCloseIngresoModal} onSubmit={handleSubmitIngresoModal}
|
||||
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
{editModalOpen && selectedBobina &&
|
||||
<StockBobinaEditFormModal
|
||||
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
|
||||
initialData={selectedBobina} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
{cambioEstadoModalOpen && selectedBobina &&
|
||||
<StockBobinaCambioEstadoModal
|
||||
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
|
||||
bobinaActual={selectedBobina} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
}
|
||||
<StockBobinaEditFormModal
|
||||
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
|
||||
initialData={selectedBobinaForRowMenu} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
<StockBobinaCambioEstadoModal
|
||||
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
|
||||
bobinaActual={selectedBobinaForRowMenu} errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
<StockBobinaLoteFormModal
|
||||
open={loteModalOpen}
|
||||
onClose={handleLoteModalClose}
|
||||
/>
|
||||
<StockBobinaFechaRemitoModal
|
||||
open={fechaRemitoModalOpen}
|
||||
onClose={handleCloseFechaRemitoModal}
|
||||
onSubmit={handleSubmitFechaRemitoModal}
|
||||
bobinaContexto={selectedBobinaForRowMenu}
|
||||
errorMessage={apiErrorMessage}
|
||||
clearErrorMessage={() => setApiErrorMessage(null)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import reportesService from '../../services/Reportes/reportesService';
|
||||
import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto';
|
||||
import SeleccionaReporteDetalleDistribucionCanillas from './SeleccionaReporteDetalleDistribucionCanillas';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import axios from 'axios';
|
||||
|
||||
// Para el tipo del footer en DataGridSectionProps
|
||||
@@ -81,9 +82,12 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
const [currentParams, setCurrentParams] = useState<{
|
||||
fecha: string;
|
||||
idEmpresa: number;
|
||||
esAccionista: boolean;
|
||||
nombreEmpresa?: string;
|
||||
} | null>(null);
|
||||
const [pdfSoloTotales, setPdfSoloTotales] = useState(false);
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
const puedeVerReporte = isSuperAdmin || tienePermiso("MC005");
|
||||
|
||||
const initialTotals: TotalesComunes = { totalCantSalida: 0, totalCantEntrada: 0, vendidos: 0, totalRendir: 0 };
|
||||
const [totalesCanillas, setTotalesCanillas] = useState<TotalesComunes>(initialTotals);
|
||||
@@ -115,16 +119,29 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
const handleGenerarReporte = useCallback(async (params: {
|
||||
fecha: string;
|
||||
idEmpresa: number;
|
||||
esAccionista: boolean;
|
||||
}) => {
|
||||
if (!puedeVerReporte) {
|
||||
setError("No tiene permiso para generar este reporte.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
|
||||
const empData = await empresaService.getEmpresaById(params.idEmpresa);
|
||||
setCurrentParams({ ...params, nombreEmpresa: empData?.nombre });
|
||||
setReportData(null);
|
||||
|
||||
// Resetear totales
|
||||
let nombreEmpresa = `Empresa ID ${params.idEmpresa}`;
|
||||
if (params.idEmpresa !== 0) {
|
||||
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
|
||||
const empData = await empresaService.getEmpresaById(params.idEmpresa);
|
||||
nombreEmpresa = empData?.nombre ?? nombreEmpresa;
|
||||
} else {
|
||||
nombreEmpresa = "TODAS";
|
||||
}
|
||||
|
||||
setCurrentParams({ ...params, nombreEmpresa });
|
||||
setReportData(null);
|
||||
setTotalesCanillas(initialTotals);
|
||||
setTotalesAccionistas(initialTotals);
|
||||
setTotalesCanillasOtraFecha(initialTotals);
|
||||
@@ -140,7 +157,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
const processedData = {
|
||||
canillas: addIds(data.canillas, 'can'),
|
||||
canillasAccionistas: addIds(data.canillasAccionistas, 'acc'),
|
||||
canillasTodos: addIds(data.canillasTodos, 'all'), // Aún necesita IDs para DataGridSection
|
||||
canillasTodos: addIds(data.canillasTodos, 'all'),
|
||||
canillasLiquidadasOtraFecha: addIds(data.canillasLiquidadasOtraFecha, 'canliq'),
|
||||
canillasAccionistasLiquidadasOtraFecha: addIds(data.canillasAccionistasLiquidadasOtraFecha, 'accliq'),
|
||||
controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'),
|
||||
@@ -167,7 +184,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [puedeVerReporte]);
|
||||
|
||||
const handleVolverAParametros = useCallback(() => {
|
||||
setShowParamSelector(true);
|
||||
@@ -188,10 +205,9 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
if (data && data.length > 0) {
|
||||
const exportedData = data.map(item => {
|
||||
const row: Record<string, any> = {};
|
||||
// Excluir el 'id' generado para DataGrid si existe
|
||||
const { id, ...itemData } = item;
|
||||
Object.keys(fields).forEach(key => {
|
||||
row[fields[key]] = (itemData as any)[key]; // Usar itemData
|
||||
row[fields[key]] = (itemData as any)[key];
|
||||
if (key === 'fecha' && (itemData as any)[key]) {
|
||||
row[fields[key]] = new Date((itemData as any)[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' });
|
||||
}
|
||||
@@ -215,18 +231,18 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Definición de campos para la exportación
|
||||
const fieldsCanillaAccionista = { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
|
||||
const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha: "Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
|
||||
const fieldsTodos = { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
|
||||
|
||||
formatAndSheet(reportData.canillas, "Canillitas_Dia", fieldsCanillaAccionista);
|
||||
formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", fieldsCanillaAccionista);
|
||||
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos);
|
||||
if (currentParams?.idEmpresa !== 0) {
|
||||
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos);
|
||||
}
|
||||
formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", fieldsCanillaAccionistaFechaLiq);
|
||||
formatAndSheet(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas_OtrasFechas", fieldsCanillaAccionistaFechaLiq);
|
||||
|
||||
|
||||
let fileName = "ReporteDetalleDistribucionCanillitas";
|
||||
if (currentParams) {
|
||||
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
|
||||
@@ -265,8 +281,6 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
}
|
||||
}, [currentParams]);
|
||||
|
||||
|
||||
// --- Definiciones de Columnas ---
|
||||
const commonColumns: GridColDef[] = [
|
||||
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
|
||||
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.3 },
|
||||
@@ -295,8 +309,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
|
||||
];
|
||||
|
||||
// --- Custom Footers ---
|
||||
const createCustomFooterComponent = (totals: TotalesComunes, columnsDef: GridColDef[]): CustomFooterType => { // Especificar el tipo de retorno
|
||||
const createCustomFooterComponent = (totals: TotalesComunes, columnsDef: GridColDef[]): CustomFooterType => {
|
||||
const getCellStyle = (colConfig: GridColDef | undefined, isPlaceholder: boolean = false) => {
|
||||
if (!colConfig) return { width: 100, textAlign: 'right' as const, pr: isPlaceholder ? 0 : 1, fontWeight: 'bold' };
|
||||
const defaultWidth = colConfig.field === 'publicacion' ? 200 : (colConfig.field === 'canilla' || colConfig.field === 'tipoVendedor' ? 150 : 100);
|
||||
@@ -310,10 +323,9 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const FooterComponent: CustomFooterType = (props) => ( // El componente debe aceptar props
|
||||
<GridFooterContainer {...props} sx={{ // Pasar props y combinar sx
|
||||
...(props.sx as any), // Castear props.sx temporalmente si es necesario
|
||||
const FooterComponent: CustomFooterType = (props) => (
|
||||
<GridFooterContainer {...props} sx={{
|
||||
...(props.sx as any),
|
||||
justifyContent: 'space-between', alignItems: 'center', width: '100%',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`, minHeight: '52px',
|
||||
}}>
|
||||
@@ -339,6 +351,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
return FooterComponent;
|
||||
};
|
||||
|
||||
// Usar los componentes creados con useMemo
|
||||
const FooterCanillas = useMemo(() => createCustomFooterComponent(totalesCanillas, commonColumns), [totalesCanillas]);
|
||||
const FooterAccionistas = useMemo(() => createCustomFooterComponent(totalesAccionistas, commonColumns), [totalesAccionistas]);
|
||||
const FooterCanillasOtraFecha = useMemo(() => createCustomFooterComponent(totalesCanillasOtraFecha, commonColumnsWithFecha), [totalesCanillasOtraFecha]);
|
||||
@@ -346,12 +359,16 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
const FooterResumen = useMemo(() => createCustomFooterComponent(totalesResumen, columnsTodos), [totalesResumen, columnsTodos]);
|
||||
|
||||
if (showParamSelector) {
|
||||
if (!loading && !puedeVerReporte) {
|
||||
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
|
||||
<SeleccionaReporteDetalleDistribucionCanillas
|
||||
onGenerarReporte={handleGenerarReporte}
|
||||
onCancel={handleVolverAParametros} // Aunque el componente no lo use directamente.
|
||||
onCancel={handleVolverAParametros}
|
||||
isLoading={loading}
|
||||
apiErrorMessage={apiErrorParams}
|
||||
/>
|
||||
@@ -360,17 +377,45 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>;
|
||||
if (error && !loading) return <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>;
|
||||
if (!reportData) return <Typography sx={{ mt: 2, fontStyle: 'italic' }}>No se encontraron datos.</Typography>;
|
||||
|
||||
if (currentParams?.idEmpresa === 0) {
|
||||
if (currentParams.esAccionista) {
|
||||
return <DataGridSection title="Accionistas (Todas las Empresas)" data={reportData.canillasAccionistas || []} columns={commonColumns} footerComponent={FooterAccionistas} />;
|
||||
}
|
||||
return <DataGridSection title="Canillitas (Todas las Empresas)" data={reportData.canillas || []} columns={commonColumns} footerComponent={FooterCanillas} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataGridSection title="Canillitas" data={reportData.canillas || []} columns={commonColumns} footerComponent={FooterCanillas} />
|
||||
<DataGridSection title="Accionistas" data={reportData.canillasAccionistas || []} columns={commonColumns} footerComponent={FooterAccionistas} />
|
||||
<DataGridSection title="Resumen por Tipo de Vendedor" data={reportData.canillasTodos || []} columns={columnsTodos} footerComponent={FooterResumen} />
|
||||
{reportData.canillasLiquidadasOtraFecha && reportData.canillasLiquidadasOtraFecha.length > 0 &&
|
||||
<DataGridSection title="Canillitas (Liquidados de Otras Fechas)" data={reportData.canillasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterCanillasOtraFecha} />}
|
||||
{reportData.canillasAccionistasLiquidadasOtraFecha && reportData.canillasAccionistasLiquidadasOtraFecha.length > 0 &&
|
||||
<DataGridSection title="Accionistas (Liquidados de Otras Fechas)" data={reportData.canillasAccionistasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterAccionistasOtraFecha} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
|
||||
<Typography variant="h5">Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', { timeZone: 'UTC' }) : ''}</Typography>
|
||||
<Typography variant="h5">Reporte: Detalle Distribución ({currentParams?.nombreEmpresa}) - {currentParams?.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', { timeZone: 'UTC' }) : ''}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
{currentParams?.idEmpresa !== 0 && (
|
||||
<Button onClick={() => handleGenerarYAbrirPdf(true)} variant="contained" color="secondary" disabled={loadingPdf || !reportData || !!error} size="small">
|
||||
{loadingPdf && pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Totales"}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => handleGenerarYAbrirPdf(false)} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
|
||||
{loadingPdf && !pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Detalle"}
|
||||
</Button>
|
||||
<Button onClick={() => handleGenerarYAbrirPdf(true)} variant="contained" color="secondary" disabled={loadingPdf || !reportData || !!error} size="small">
|
||||
{loadingPdf && pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Totales"}
|
||||
</Button>
|
||||
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small">
|
||||
Exportar a Excel
|
||||
</Button>
|
||||
@@ -379,34 +424,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && <Box sx={{ textAlign: 'center', my: 2 }}><CircularProgress /></Box>}
|
||||
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
|
||||
|
||||
{!loading && !error && reportData && (
|
||||
<>
|
||||
<DataGridSection title="Canillitas" data={reportData.canillas || []} columns={commonColumns} footerComponent={FooterCanillas} />
|
||||
<DataGridSection title="Accionistas" data={reportData.canillasAccionistas || []} columns={commonColumns} footerComponent={FooterAccionistas} />
|
||||
|
||||
<DataGridSection
|
||||
title="Resumen por Tipo de Vendedor"
|
||||
data={reportData.canillasTodos || []}
|
||||
columns={columnsTodos}
|
||||
footerComponent={FooterResumen} // <-- PASAR EL FOOTER
|
||||
height={220} // El height ya no es necesario si autoHeight está activado por tener footer
|
||||
/>
|
||||
|
||||
{reportData.canillasLiquidadasOtraFecha && reportData.canillasLiquidadasOtraFecha.length > 0 &&
|
||||
<DataGridSection title="Canillitas (Liquidados de Otras Fechas)" data={reportData.canillasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterCanillasOtraFecha} />}
|
||||
|
||||
{reportData.canillasAccionistasLiquidadasOtraFecha && reportData.canillasAccionistasLiquidadasOtraFecha.length > 0 &&
|
||||
<DataGridSection title="Accionistas (Liquidados de Otras Fechas)" data={reportData.canillasAccionistasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterAccionistasOtraFecha} />}
|
||||
</>
|
||||
)}
|
||||
{!loading && !error && reportData &&
|
||||
Object.values(reportData).every(arr => !arr || arr.length === 0) &&
|
||||
<Typography sx={{ mt: 2, fontStyle: 'italic' }}>No se encontraron datos para los criterios seleccionados.</Typography>
|
||||
}
|
||||
{renderContent()}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,8 +69,8 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
setReportData(null);
|
||||
setDetalleDiarioCalculado([]);
|
||||
setPromediosPorDiaCalculado([]);
|
||||
setTotalesDetalle({ llevados:0, devueltos:0, ventaNeta:0, promedioGeneralVentaNeta:0, porcentajeDevolucionGeneral:0 });
|
||||
setTotalesPromedios({ cantDias:0, promLlevados:0, promDevueltos:0, promVentas:0, porcentajeDevolucionGeneral:0});
|
||||
setTotalesDetalle({ llevados: 0, devueltos: 0, ventaNeta: 0, promedioGeneralVentaNeta: 0, porcentajeDevolucionGeneral: 0 });
|
||||
setTotalesPromedios({ cantDias: 0, promLlevados: 0, promDevueltos: 0, promVentas: 0, porcentajeDevolucionGeneral: 0 });
|
||||
|
||||
|
||||
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
|
||||
@@ -112,43 +112,42 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
const totalVentaNetaDetalle = totalLlevadosDetalle - totalDevueltosDetalle;
|
||||
|
||||
setTotalesDetalle({
|
||||
llevados: totalLlevadosDetalle,
|
||||
devueltos: totalDevueltosDetalle,
|
||||
ventaNeta: totalVentaNetaDetalle,
|
||||
promedioGeneralVentaNeta: ultimoPromedioDetalle,
|
||||
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
|
||||
llevados: totalLlevadosDetalle,
|
||||
devueltos: totalDevueltosDetalle,
|
||||
ventaNeta: totalVentaNetaDetalle,
|
||||
promedioGeneralVentaNeta: ultimoPromedioDetalle,
|
||||
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
|
||||
});
|
||||
|
||||
|
||||
// --- Cálculos para promedios y sus totales ---
|
||||
const promediosCalculadoLocal = data.promediosPorDia.map((item, index) => {
|
||||
const promLlevados = item.promedio_Llevados || 0;
|
||||
const promDevueltos = item.promedio_Devueltos || 0;
|
||||
const promVentas = item.promedio_Ventas || 0;
|
||||
return {
|
||||
...item,
|
||||
id: `prom-can-${index}`, // o prom-dist-${index}
|
||||
// LA COLUMNA EN EL PDF SE LLAMA "% Devolución" PERO PARECE SER "% VENTA"
|
||||
id: `prom-can-${index}`,
|
||||
porcentajeColumnaPDF: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0,
|
||||
porcentajeDevolucion: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0,
|
||||
porcentajeDevolucion: promLlevados > 0 ? (promDevueltos / promLlevados) * 100 : 0,
|
||||
};
|
||||
});
|
||||
setPromediosPorDiaCalculado(promediosCalculadoLocal);
|
||||
|
||||
const totalDiasProm = promediosCalculadoLocal.reduce((sum, item) => sum + (item.cant || 0), 0);
|
||||
const totalPonderadoLlevados = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Llevados || 0) * (item.cant || 0)), 0);
|
||||
// const totalPonderadoDevueltos = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Devueltos || 0) * (item.cant || 0)), 0); // No se usa para el % del PDF
|
||||
const totalPonderadoDevueltos = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Devueltos || 0) * (item.cant || 0)), 0);
|
||||
const totalPonderadoVentas = promediosCalculadoLocal.reduce((sum, item) => sum + ((item.promedio_Ventas || 0) * (item.cant || 0)), 0);
|
||||
|
||||
const promGeneralLlevados = totalDiasProm > 0 ? totalPonderadoLlevados / totalDiasProm : 0;
|
||||
const promGeneralDevueltos = totalDiasProm > 0 ? totalPonderadoDevueltos / totalDiasProm : 0;
|
||||
|
||||
setTotalesPromedios({
|
||||
cantDias: totalDiasProm,
|
||||
promLlevados: totalDiasProm > 0 ? totalPonderadoLlevados / totalDiasProm : 0,
|
||||
promDevueltos: totalDiasProm > 0 ? promediosCalculadoLocal.reduce((sum, item) => sum + (item.promedio_Devueltos || 0), 0) / promediosCalculadoLocal.length :0, // Promedio simple para mostrar
|
||||
promLlevados: promGeneralLlevados,
|
||||
promDevueltos: promGeneralDevueltos,
|
||||
promVentas: totalDiasProm > 0 ? totalPonderadoVentas / totalDiasProm : 0,
|
||||
// Para la fila "General" de promedios, el PDF usa (Total Prom. Ventas / Total Prom. Llevados) * 100
|
||||
// Usaremos los promedios generales calculados aquí
|
||||
porcentajeDevolucionGeneral: (totalDiasProm > 0 && (totalPonderadoLlevados / totalDiasProm) > 0)
|
||||
? ((totalPonderadoVentas / totalDiasProm) / (totalPonderadoLlevados / totalDiasProm)) * 100
|
||||
: 0,
|
||||
porcentajeDevolucionGeneral: promGeneralLlevados > 0 ? (promGeneralDevueltos / promGeneralLlevados) * 100 : 0,
|
||||
});
|
||||
|
||||
setReportData({ detalleSimple: detalleCalculadoLocal, promediosPorDia: promediosCalculadoLocal });
|
||||
@@ -280,11 +279,11 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
</Box>
|
||||
{/* Contenedor para tus totales */}
|
||||
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', whiteSpace: 'nowrap', overflowX: 'auto' }}>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold'}}>General:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[1].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.llevados.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.devueltos.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.ventaNeta.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr:1 }}>{totalesDetalle.promedioGeneralVentaNeta.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold' }}>General:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[1].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.llevados.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.devueltos.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.ventaNeta.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', mr: 1 }}>{totalesDetalle.promedioGeneralVentaNeta.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesDetalle.porcentajeDevolucionGeneral.toLocaleString('es-AR', { maximumFractionDigits: 2 })}%</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
@@ -297,11 +296,11 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
<GridFooter sx={{ borderTop: 'none' }} />
|
||||
</Box>
|
||||
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto' }}>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold'}}>General:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsPromedios[1].width || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.cantDias.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr:1 }}>{totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[0].width || 'auto', textAlign: 'right', fontWeight: 'bold' }}>General:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ width: columnsPromedios[1].width || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.cantDias.toLocaleString('es-AR')}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[2].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[3].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[4].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold', pr: 1 }}>{totalesPromedios.promVentas.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Typography>
|
||||
</Box>
|
||||
</GridFooterContainer>
|
||||
@@ -372,10 +371,10 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
|
||||
density="compact"
|
||||
slots={{ footer: CustomFooterPromedios }}
|
||||
hideFooterSelectedRowCount
|
||||
sx={{
|
||||
'& .MuiTablePagination-root': { // Oculta el paginador por defecto
|
||||
display: 'none',
|
||||
},
|
||||
sx={{
|
||||
'& .MuiTablePagination-root': { // Oculta el paginador por defecto
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
@@ -121,20 +121,24 @@ const ReporteListadoDistribucionPage: React.FC = () => {
|
||||
porcentajeDevolucion: item.promedio_Llevados > 0 ? (item.promedio_Devueltos / item.promedio_Llevados) * 100 : 0,
|
||||
}));
|
||||
|
||||
// Calcular totales para la tabla de promedios (ponderados por Cant. Días)
|
||||
const totalDiasPromedios = promediosConCalculos.reduce((sum, item) => sum + (item.cant || 0), 0);
|
||||
const totalPonderadoLlevados = promediosConCalculos.reduce((sum, item) => sum + ((item.promedio_Llevados || 0) * (item.cant || 0)), 0);
|
||||
const totalPonderadoDevueltos = promediosConCalculos.reduce((sum, item) => sum + ((item.promedio_Devueltos || 0) * (item.cant || 0)), 0);
|
||||
const totalPonderadoVentas = promediosConCalculos.reduce((sum, item) => sum + ((item.promedio_Ventas || 0) * (item.cant || 0)), 0);
|
||||
const countPromedios = promediosConCalculos.length;
|
||||
|
||||
setTotalesPromedios({
|
||||
cantDias: totalDiasPromedios,
|
||||
promLlevados: totalDiasPromedios > 0 ? totalPonderadoLlevados / totalDiasPromedios : 0,
|
||||
promDevueltos: totalDiasPromedios > 0 ? totalPonderadoDevueltos / totalDiasPromedios : 0,
|
||||
promVentas: totalDiasPromedios > 0 ? totalPonderadoVentas / totalDiasPromedios : 0,
|
||||
porcentajeDevolucionGeneral: totalPonderadoLlevados > 0 ? (totalPonderadoDevueltos / totalPonderadoLlevados) * 100 : 0,
|
||||
});
|
||||
// LÓGICA DE PROMEDIO DE PROMEDIOS
|
||||
if (countPromedios > 0) {
|
||||
const sumPromLlevados = promediosConCalculos.reduce((sum, item) => sum + (item.promedio_Llevados || 0), 0);
|
||||
const sumPromDevueltos = promediosConCalculos.reduce((sum, item) => sum + (item.promedio_Devueltos || 0), 0);
|
||||
const sumPromVentas = promediosConCalculos.reduce((sum, item) => sum + (item.promedio_Ventas || 0), 0);
|
||||
const sumPorcDevolucion = promediosConCalculos.reduce((sum, item) => sum + (item.porcentajeDevolucion || 0), 0);
|
||||
|
||||
setTotalesPromedios({
|
||||
cantDias: totalDiasPromedios,
|
||||
promLlevados: sumPromLlevados / countPromedios,
|
||||
promDevueltos: sumPromDevueltos / countPromedios,
|
||||
promVentas: sumPromVentas / countPromedios,
|
||||
porcentajeDevolucionGeneral: sumPorcDevolucion / countPromedios,
|
||||
});
|
||||
}
|
||||
|
||||
setReportData({ detalleSimple: detalleConCalculos, promediosPorDia: promediosConCalculos });
|
||||
|
||||
|
||||
@@ -3,92 +3,80 @@ import { Box, Paper, Typography, List, ListItemButton, ListItemText, Collapse, C
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
|
||||
// Definición de los módulos de reporte con sus categorías, etiquetas y rutas
|
||||
const allReportModules: { category: string; label: string; path: string }[] = [
|
||||
{ category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel' },
|
||||
{ category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' },
|
||||
{ category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' },
|
||||
{ category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores' },
|
||||
{ category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas' },
|
||||
{ category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general' },
|
||||
{ category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe' },
|
||||
{ category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas' },
|
||||
{ category: 'Secretaría', label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria' },
|
||||
{ category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones' },
|
||||
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion' },
|
||||
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/PubPublicación', path: 'consumo-bobinas-publicacion' },
|
||||
{ category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas' },
|
||||
{ category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' },
|
||||
{ category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones' },
|
||||
{ category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas' },
|
||||
{ category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual' },
|
||||
{ category: 'Suscripciones', label: 'Facturas para Publicidad', path: 'suscripciones-facturas-publicidad' },
|
||||
{ category: 'Suscripciones', label: 'Distribución de Suscripciones', path: 'suscripciones-distribucion' },
|
||||
const allReportModules: { category: string; label: string; path: string; requiredPermission: string; }[] = [
|
||||
{ category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel', requiredPermission: 'RR005' },
|
||||
{ category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas', requiredPermission: 'RR006' },
|
||||
{ category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado', requiredPermission: 'RR006' },
|
||||
{ category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores', requiredPermission: 'RR002' },
|
||||
{ category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas', requiredPermission: 'RR002' },
|
||||
{ category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general', requiredPermission: 'RR002' },
|
||||
{ category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe', requiredPermission: 'RR002' },
|
||||
{ category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas', requiredPermission: 'MC005' },
|
||||
{ category: 'Secretaría', label: 'Reportes de Ventas', path: 'venta-mensual-secretaria', requiredPermission: 'RR012' },
|
||||
{ category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones', requiredPermission: 'RR008' },
|
||||
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion', requiredPermission: 'RR007' },
|
||||
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/Publicación', path: 'consumo-bobinas-publicacion', requiredPermission: 'RR007' },
|
||||
{ category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas', requiredPermission: 'RR007' },
|
||||
{ category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores', requiredPermission: 'RR001' },
|
||||
{ category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones', requiredPermission: 'RR003' },
|
||||
{ category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas', requiredPermission: 'RR004' },
|
||||
{ category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual', requiredPermission: 'RR009' },
|
||||
{ category: 'Suscripciones', label: 'Facturas para Publicidad', path: 'suscripciones-facturas-publicidad', requiredPermission: 'RR010' },
|
||||
{ category: 'Suscripciones', label: 'Distribución de Suscripciones', path: 'suscripciones-distribucion', requiredPermission: 'RR011' },
|
||||
];
|
||||
|
||||
const predefinedCategoryOrder = [
|
||||
'Balance de Cuentas',
|
||||
'Listados Distribución',
|
||||
'Ctrl. Devoluciones',
|
||||
'Novedades de Canillitas',
|
||||
'Suscripciones',
|
||||
'Existencia Papel',
|
||||
'Movimientos Bobinas',
|
||||
'Consumos Bobinas',
|
||||
'Tiradas por Publicación',
|
||||
'Secretaría',
|
||||
'Balance de Cuentas', 'Listados Distribución', 'Ctrl. Devoluciones',
|
||||
'Novedades de Canillitas', 'Suscripciones', 'Existencia Papel',
|
||||
'Movimientos Bobinas', 'Consumos Bobinas', 'Tiradas por Publicación', 'Secretaría',
|
||||
];
|
||||
|
||||
|
||||
const ReportesIndexPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const [expandedCategory, setExpandedCategory] = useState<string | false>(false);
|
||||
const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true);
|
||||
const { tienePermiso, isSuperAdmin } = usePermissions();
|
||||
|
||||
const uniqueCategories = useMemo(() => predefinedCategoryOrder, []);
|
||||
const accessibleReportModules = useMemo(() => {
|
||||
return allReportModules.filter(module =>
|
||||
isSuperAdmin || tienePermiso(module.requiredPermission)
|
||||
);
|
||||
}, [isSuperAdmin, tienePermiso]);
|
||||
|
||||
const accessibleCategories = useMemo(() => {
|
||||
const categoriesWithAccess = new Set(accessibleReportModules.map(r => r.category));
|
||||
return predefinedCategoryOrder.filter(category => categoriesWithAccess.has(category));
|
||||
}, [accessibleReportModules]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentBasePath = '/reportes';
|
||||
const pathParts = location.pathname.substring(currentBasePath.length + 1).split('/');
|
||||
const subPathSegment = pathParts[0];
|
||||
|
||||
let activeReportFoundInEffect = false;
|
||||
|
||||
if (subPathSegment && subPathSegment !== "") { // Asegurarse que subPathSegment no esté vacío
|
||||
const activeReport = allReportModules.find(module => module.path === subPathSegment);
|
||||
if (subPathSegment) {
|
||||
const activeReport = accessibleReportModules.find(module => module.path === subPathSegment);
|
||||
if (activeReport) {
|
||||
setExpandedCategory(activeReport.category);
|
||||
activeReportFoundInEffect = true;
|
||||
} else {
|
||||
// Si la URL apunta a un reporte que no es accesible, no expandimos nada
|
||||
setExpandedCategory(false);
|
||||
}
|
||||
} else {
|
||||
setExpandedCategory(false);
|
||||
// Si estamos en la página base (/reportes), expandimos la primera categoría disponible.
|
||||
if (accessibleCategories.length > 0) {
|
||||
setExpandedCategory(accessibleCategories[0]);
|
||||
} else {
|
||||
setExpandedCategory(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (location.pathname === currentBasePath && allReportModules.length > 0 && isLoadingInitialNavigation) {
|
||||
let firstReportToNavigate: { category: string; label: string; path: string } | null = null;
|
||||
for (const category of uniqueCategories) {
|
||||
const reportsInCat = allReportModules.filter(r => r.category === category);
|
||||
if (reportsInCat.length > 0) {
|
||||
firstReportToNavigate = reportsInCat[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstReportToNavigate) {
|
||||
navigate(firstReportToNavigate.path, { replace: true });
|
||||
activeReportFoundInEffect = true;
|
||||
}
|
||||
}
|
||||
// Solo se establece a false si no estamos en el proceso de navegación inicial O si no se encontró reporte
|
||||
if (!activeReportFoundInEffect || location.pathname !== currentBasePath) {
|
||||
setIsLoadingInitialNavigation(false);
|
||||
}
|
||||
// No hay navegación automática, solo manejamos el estado de carga.
|
||||
setIsLoadingInitialNavigation(false);
|
||||
|
||||
}, [location.pathname, navigate, uniqueCategories, isLoadingInitialNavigation]);
|
||||
}, [location.pathname, accessibleReportModules, accessibleCategories]);
|
||||
|
||||
const handleCategoryClick = (categoryName: string) => {
|
||||
setExpandedCategory(prev => (prev === categoryName ? false : categoryName));
|
||||
@@ -99,12 +87,10 @@ const ReportesIndexPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const isReportActive = (reportPath: string) => {
|
||||
return location.pathname === `/reportes/${reportPath}` || location.pathname.startsWith(`/reportes/${reportPath}/`);
|
||||
return location.pathname.startsWith(`/reportes/${reportPath}`);
|
||||
};
|
||||
|
||||
// Si isLoadingInitialNavigation es true Y estamos en /reportes, mostrar loader
|
||||
// Esto evita mostrar el loader si se navega directamente a un sub-reporte.
|
||||
if (isLoadingInitialNavigation && (location.pathname === '/reportes' || location.pathname === '/reportes/')) {
|
||||
if (isLoadingInitialNavigation) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}>
|
||||
<CircularProgress />
|
||||
@@ -113,44 +99,25 @@ const ReportesIndexPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
// Contenedor principal que se adaptará a su padre
|
||||
// Eliminamos 'height: calc(100vh - 64px)' y cualquier margen/padding que controle el espacio exterior
|
||||
<Box sx={{ display: 'flex', width: '100%', height: '100%' }}>
|
||||
{/* Panel Lateral para Navegación */}
|
||||
<Paper
|
||||
elevation={0} // Sin elevación para que se sienta más integrado si el fondo es el mismo
|
||||
square // Bordes rectos
|
||||
sx={{
|
||||
width: { xs: 220, sm: 250, md: 280 }, // Ancho responsivo del panel lateral
|
||||
<Paper elevation={0} square sx={{
|
||||
width: { xs: 220, sm: 250, md: 280 },
|
||||
minWidth: { xs: 200, sm: 220 },
|
||||
height: '100%', // Ocupa toda la altura del Box padre
|
||||
height: '100%',
|
||||
borderRight: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
overflowY: 'auto',
|
||||
bgcolor: 'background.paper', // O el color que desees para el menú
|
||||
// display: 'flex', flexDirection: 'column' // Para que el título y la lista usen el espacio vertical
|
||||
}}
|
||||
>
|
||||
{/* Título del Menú Lateral */}
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5, // Padding interno para el título
|
||||
// borderBottom: (theme) => `1px solid ${theme.palette.divider}`, // Opcional: separador
|
||||
// position: 'sticky', // Si quieres que el título quede fijo al hacer scroll en la lista
|
||||
// top: 0,
|
||||
// zIndex: 1,
|
||||
// bgcolor: 'background.paper' // Necesario si es sticky y tiene scroll la lista
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 /* Pequeño margen para alinear con items */ }}>
|
||||
bgcolor: 'background.paper',
|
||||
}}>
|
||||
<Box sx={{ p: 1.5 }}>
|
||||
<Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 }}>
|
||||
Reportes
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Lista de Categorías y Reportes */}
|
||||
{uniqueCategories.length > 0 ? (
|
||||
<List component="nav" dense sx={{ pt: 0 }} /* Quitar padding superior de la lista si el título ya lo tiene */ >
|
||||
{uniqueCategories.map((category) => {
|
||||
const reportsInCategory = allReportModules.filter(r => r.category === category);
|
||||
{accessibleCategories.length > 0 ? (
|
||||
<List component="nav" dense sx={{ pt: 0 }}>
|
||||
{accessibleCategories.map((category) => {
|
||||
const reportsInCategory = accessibleReportModules.filter(r => r.category === category);
|
||||
const isExpanded = expandedCategory === category;
|
||||
|
||||
return (
|
||||
@@ -158,26 +125,14 @@ const ReportesIndexPage: React.FC = () => {
|
||||
<ListItemButton
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
sx={{
|
||||
// py: 1.2, // Ajustar padding vertical de items de categoría
|
||||
// backgroundColor: isExpanded ? 'action.selected' : 'transparent',
|
||||
borderLeft: isExpanded ? (theme) => `4px solid ${theme.palette.primary.main}` : '4px solid transparent',
|
||||
pr: 1, // Menos padding a la derecha para dar espacio al ícono expander
|
||||
'&:hover': {
|
||||
backgroundColor: 'action.hover'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={category}
|
||||
primaryTypographyProps={{
|
||||
fontWeight: isExpanded ? 'bold' : 'normal',
|
||||
// color: isExpanded ? 'primary.main' : 'text.primary'
|
||||
}}
|
||||
/>
|
||||
{reportsInCategory.length > 0 && (isExpanded ? <ExpandLess /> : <ExpandMore />)}
|
||||
pr: 1,
|
||||
'&:hover': { backgroundColor: 'action.hover' }
|
||||
}}>
|
||||
<ListItemText primary={category} primaryTypographyProps={{ fontWeight: isExpanded ? 'bold' : 'normal' }}/>
|
||||
{isExpanded ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItemButton>
|
||||
{reportsInCategory.length > 0 && (
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding dense>
|
||||
{reportsInCategory.map((report) => (
|
||||
<ListItemButton
|
||||
@@ -185,62 +140,39 @@ const ReportesIndexPage: React.FC = () => {
|
||||
selected={isReportActive(report.path)}
|
||||
onClick={() => handleReportClick(report.path)}
|
||||
sx={{
|
||||
pl: 3.5, // Indentación para los reportes (ajustar si se cambió el padding del título)
|
||||
py: 0.8, // Padding vertical de items de reporte
|
||||
pl: 3.5, py: 0.8,
|
||||
...(isReportActive(report.path) && {
|
||||
backgroundColor: (theme) => theme.palette.action.selected, // Un color de fondo sutil
|
||||
borderLeft: (theme) => `4px solid ${theme.palette.primary.light}`, // Un borde para el activo
|
||||
'& .MuiListItemText-primary': {
|
||||
fontWeight: 'medium', // O 'bold'
|
||||
// color: 'primary.main'
|
||||
},
|
||||
backgroundColor: (theme) => theme.palette.action.selected,
|
||||
borderLeft: (theme) => `4px solid ${theme.palette.primary.light}`,
|
||||
'& .MuiListItemText-primary': { fontWeight: 'medium' },
|
||||
}),
|
||||
'&:hover': {
|
||||
backgroundColor: (theme) => theme.palette.action.hover
|
||||
}
|
||||
}}
|
||||
>
|
||||
'&:hover': { backgroundColor: (theme) => theme.palette.action.hover }
|
||||
}}>
|
||||
<ListItemText primary={report.label} primaryTypographyProps={{ variant: 'body2' }}/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
)}
|
||||
{reportsInCategory.length === 0 && isExpanded && (
|
||||
<ListItemText
|
||||
primary="No hay reportes en esta categoría."
|
||||
sx={{ pl: 3.5, fontStyle: 'italic', color: 'text.secondary', py:1, typography: 'body2' }}
|
||||
/>
|
||||
)}
|
||||
</Collapse>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
) : (
|
||||
<Typography sx={{p:2, fontStyle: 'italic'}}>No hay categorías configuradas.</Typography>
|
||||
<Typography sx={{p:2, fontStyle: 'italic'}}>No tiene acceso a ningún reporte.</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Área Principal para el Contenido del Reporte */}
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1, // Ocupa el espacio restante
|
||||
p: { xs: 1, sm: 2, md: 3 }, // Padding interno para el contenido, responsivo
|
||||
overflowY: 'auto',
|
||||
height: '100%', // Ocupa toda la altura del Box padre
|
||||
bgcolor: 'grey.100' // Un color de fondo diferente para distinguir el área de contenido
|
||||
}}
|
||||
>
|
||||
{/* El Outlet renderiza el componente del reporte específico */}
|
||||
{(!location.pathname.startsWith('/reportes/') || !allReportModules.some(r => isReportActive(r.path))) && location.pathname !== '/reportes/' && location.pathname !== '/reportes' && !isLoadingInitialNavigation && (
|
||||
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
|
||||
El reporte solicitado no existe o la ruta no es válida.
|
||||
</Typography>
|
||||
)}
|
||||
{(location.pathname === '/reportes/' || location.pathname === '/reportes') && !allReportModules.some(r => isReportActive(r.path)) && !isLoadingInitialNavigation && (
|
||||
<Box component="main" sx={{
|
||||
flexGrow: 1, p: { xs: 1, sm: 2, md: 3 },
|
||||
overflowY: 'auto', height: '100%', bgcolor: 'grey.100'
|
||||
}}>
|
||||
{/* Lógica para mostrar el mensaje de bienvenida */}
|
||||
{location.pathname === '/reportes' && !isLoadingInitialNavigation && (
|
||||
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
|
||||
{allReportModules.length > 0 ? "Seleccione una categoría y un reporte del menú lateral." : "No hay reportes configurados."}
|
||||
{accessibleReportModules.length > 0
|
||||
? "Seleccione una categoría y un reporte del menú lateral."
|
||||
: "No tiene acceso a ningún reporte."
|
||||
}
|
||||
</Typography>
|
||||
)}
|
||||
<Outlet />
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
FormControl, InputLabel, Select, MenuItem, ToggleButtonGroup, ToggleButton
|
||||
} from '@mui/material';
|
||||
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto';
|
||||
import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
|
||||
import empresaService from '../../services/Distribucion/empresaService';
|
||||
|
||||
interface SeleccionaReporteDetalleDistribucionCanillasProps {
|
||||
onGenerarReporte: (params: {
|
||||
fecha: string;
|
||||
idEmpresa: number;
|
||||
// soloTotales: boolean; // Podríamos añadirlo si el usuario elige la versión del PDF
|
||||
esAccionista: boolean; // Añadimos este parámetro
|
||||
}) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
isLoading?: boolean;
|
||||
@@ -24,9 +24,9 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC<SeleccionaReporteDe
|
||||
}) => {
|
||||
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [idEmpresa, setIdEmpresa] = useState<number | string>('');
|
||||
// const [soloTotales, setSoloTotales] = useState<boolean>(false); // Si se añade la opción
|
||||
const [esAccionista, setEsAccionista] = useState<boolean>(false); // Nuevo estado
|
||||
|
||||
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]);
|
||||
const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
@@ -34,8 +34,9 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC<SeleccionaReporteDe
|
||||
const fetchEmpresas = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const data = await empresaService.getAllEmpresas(); // Asume que este servicio existe
|
||||
setEmpresas(data);
|
||||
const data = await empresaService.getEmpresasDropdown();
|
||||
// Añadimos la opción "TODAS" al principio
|
||||
setEmpresas([{ idEmpresa: 0, nombre: 'TODAS' }, ...data]);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar empresas:", error);
|
||||
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar empresas.' }));
|
||||
@@ -49,7 +50,8 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC<SeleccionaReporteDe
|
||||
const validate = (): boolean => {
|
||||
const errors: { [key: string]: string | null } = {};
|
||||
if (!fecha) errors.fecha = 'La fecha es obligatoria.';
|
||||
if (!idEmpresa) errors.idEmpresa = 'Debe seleccionar una empresa.';
|
||||
// El idEmpresa ya no puede estar vacío, porque se preselecciona "TODAS" o una empresa
|
||||
if (idEmpresa === '') errors.idEmpresa = 'Debe seleccionar una empresa.';
|
||||
setLocalErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
@@ -59,14 +61,14 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC<SeleccionaReporteDe
|
||||
onGenerarReporte({
|
||||
fecha,
|
||||
idEmpresa: Number(idEmpresa),
|
||||
// soloTotales // Si se añade la opción
|
||||
esAccionista // Pasamos el nuevo parámetro
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}>
|
||||
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 420 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Parámetros: Detalle Distribución Canillitas
|
||||
Parámetros: Detalle Distribución Canillas
|
||||
</Typography>
|
||||
<TextField
|
||||
label="Fecha"
|
||||
@@ -89,26 +91,32 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC<SeleccionaReporteDe
|
||||
value={idEmpresa}
|
||||
onChange={(e) => { setIdEmpresa(e.target.value as number); setLocalErrors(p => ({ ...p, idEmpresa: null })); }}
|
||||
>
|
||||
<MenuItem value="" disabled><em>Seleccione una empresa</em></MenuItem>
|
||||
{empresas.map((e) => (
|
||||
<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{localErrors.idEmpresa && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idEmpresa}</Typography>}
|
||||
</FormControl>
|
||||
{/*
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={soloTotales}
|
||||
onChange={(e) => setSoloTotales(e.target.checked)}
|
||||
|
||||
{/* Selector condicional para Canillitas/Accionistas */}
|
||||
{idEmpresa === 0 && (
|
||||
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'medium' }}>
|
||||
Mostrar para todas las empresas
|
||||
</Typography>
|
||||
<ToggleButtonGroup
|
||||
value={esAccionista ? 'accionistas' : 'canillitas'}
|
||||
exclusive
|
||||
onChange={(_, value) => { if (value !== null) setEsAccionista(value === 'accionistas'); }}
|
||||
aria-label="Tipo de Vendedor"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
}
|
||||
label="Generar solo resumen de totales (PDF)"
|
||||
sx={{ mt: 1, mb: 1 }}
|
||||
/>
|
||||
*/}
|
||||
color="primary"
|
||||
>
|
||||
<ToggleButton value="canillitas">Canillitas</ToggleButton>
|
||||
<ToggleButton value="accionistas">Accionistas</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
|
||||
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
Box, Typography, TextField, Button, CircularProgress, Alert,
|
||||
FormControl, InputLabel, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
|
||||
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
|
||||
import publicacionService from '../../services/Distribucion/publicacionService';
|
||||
|
||||
interface SeleccionaReporteListadoDistribucionCanillasProps {
|
||||
@@ -26,7 +26,7 @@ const SeleccionaReporteListadoDistribucionCanillas: React.FC<SeleccionaReporteLi
|
||||
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
|
||||
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
|
||||
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
|
||||
|
||||
@@ -34,7 +34,7 @@ const SeleccionaReporteListadoDistribucionCanillas: React.FC<SeleccionaReporteLi
|
||||
const fetchPublicaciones = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const data = await publicacionService.getAllPublicaciones(undefined, undefined);
|
||||
const data = await publicacionService.getPublicacionesForDropdown(undefined);
|
||||
setPublicaciones(data.map(p => p));
|
||||
} catch (error) {
|
||||
console.error("Error al cargar publicaciones:", error);
|
||||
|
||||
@@ -24,8 +24,9 @@ const meses = [
|
||||
{ value: 10, label: 'Octubre' }, { value: 11, label: 'Noviembre' }, { value: 12, label: 'Diciembre' }
|
||||
];
|
||||
|
||||
const estadosPago = ['Pendiente', 'Pagada', 'Rechazada', 'Anulada'];
|
||||
const estadosPago = ['Pendiente', 'Pagada', 'Pagada Parcialmente', 'Rechazada', 'Anulada'];
|
||||
const estadosFacturacion = ['Pendiente de Facturar', 'Facturado'];
|
||||
const tiposFactura = ['Mensual', 'Alta'];
|
||||
|
||||
const SuscriptorRow: React.FC<{
|
||||
resumen: ResumenCuentaSuscriptorDto;
|
||||
@@ -33,50 +34,60 @@ const SuscriptorRow: React.FC<{
|
||||
handleOpenHistorial: (factura: FacturaConsolidadaDto) => void;
|
||||
}> = ({ resumen, handleMenuOpen, handleOpenHistorial }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const formatCurrency = (value: number) => `$${value.toFixed(2)}`;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow sx={{ '& > *': { borderBottom: 'unset' } }} hover>
|
||||
<TableCell><IconButton size="small" onClick={() => setOpen(!open)}>{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}</IconButton></TableCell>
|
||||
<TableCell component="th" scope="row">{resumen.nombreSuscriptor}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: resumen.saldoPendienteTotal > 0 ? 'error.main' : 'success.main' }}>${resumen.saldoPendienteTotal.toFixed(2)}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">de ${resumen.importeTotal.toFixed(2)}</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: resumen.saldoPendienteTotal > 0 ? 'error.main' : 'success.main' }}>{formatCurrency(resumen.saldoPendienteTotal)}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">de {formatCurrency(resumen.importeTotal)}</Typography>
|
||||
</TableCell>
|
||||
<TableCell colSpan={5}></TableCell>
|
||||
<TableCell colSpan={6}></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={8}>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={9}> {/* <-- Ajustado para la nueva columna */}
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ margin: 1, padding: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
|
||||
<Typography variant="h6" gutterBottom component="div" sx={{ fontSize: '1rem', fontWeight: 'bold' }}>Facturas del Período para {resumen.nombreSuscriptor}</Typography>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Empresa</TableCell><TableCell align="right">Importe</TableCell>
|
||||
<TableCell>Estado Pago</TableCell><TableCell>Estado Facturación</TableCell>
|
||||
<TableCell>Nro. Factura</TableCell><TableCell align="right">Acciones</TableCell>
|
||||
<TableCell>Empresa</TableCell>
|
||||
<TableCell align="right">Importe Total</TableCell>
|
||||
<TableCell align="right">Pagado</TableCell>
|
||||
<TableCell align="right">Saldo</TableCell>
|
||||
<TableCell>Tipo Factura</TableCell>
|
||||
<TableCell>Estado Pago</TableCell>
|
||||
<TableCell>Estado Facturación</TableCell>
|
||||
<TableCell>Nro. Factura</TableCell>
|
||||
<TableCell align="right">Acciones</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{resumen.facturas.map((factura) => (
|
||||
<TableRow key={factura.idFactura}>
|
||||
<TableCell sx={{ fontWeight: 'medium' }}>{factura.nombreEmpresa}</TableCell>
|
||||
<TableCell align="right">${factura.importeFinal.toFixed(2)}</TableCell>
|
||||
<TableCell><Chip label={factura.estadoPago} size="small" color={factura.estadoPago === 'Pagada' ? 'success' : (factura.estadoPago === 'Rechazada' ? 'error' : 'default')} /></TableCell>
|
||||
<TableCell><Chip label={factura.estadoFacturacion} size="small" color={factura.estadoFacturacion === 'Facturado' ? 'info' : 'warning'} /></TableCell>
|
||||
<TableCell>{factura.numeroFactura || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, factura)} disabled={factura.estadoPago === 'Anulada'}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Tooltip title="Ver Historial de Envíos">
|
||||
<IconButton onClick={() => handleOpenHistorial(factura)}>
|
||||
<MailOutlineIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{resumen.facturas.map((factura) => {
|
||||
const saldo = factura.importeFinal - factura.totalPagado;
|
||||
return (
|
||||
<TableRow key={factura.idFactura}>
|
||||
<TableCell sx={{ fontWeight: 'medium' }}>{factura.nombreEmpresa}</TableCell>
|
||||
<TableCell align="right">{formatCurrency(factura.importeFinal)}</TableCell>
|
||||
<TableCell align="right" sx={{ color: 'success.dark' }}>{formatCurrency(factura.totalPagado)}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold', color: saldo > 0 ? 'error.main' : 'inherit' }}>{formatCurrency(saldo)}</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={factura.tipoFactura} size="small" color={factura.tipoFactura === 'Alta' ? 'secondary' : 'default'} />
|
||||
</TableCell>
|
||||
<TableCell><Chip label={factura.estadoPago} size="small" color={factura.estadoPago === 'Pagada' ? 'success' : (factura.estadoPago === 'Pagada Parcialmente' ? 'primary' : (factura.estadoPago === 'Rechazada' ? 'error' : 'default'))} /></TableCell>
|
||||
<TableCell><Chip label={factura.estadoFacturacion} size="small" color={factura.estadoFacturacion === 'Facturado' ? 'info' : 'warning'} /></TableCell>
|
||||
<TableCell>{factura.numeroFactura || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={(e) => handleMenuOpen(e, factura)} disabled={factura.estadoPago === 'Anulada'}><MoreVertIcon /></IconButton>
|
||||
<Tooltip title="Ver Historial de Envíos"><IconButton onClick={() => handleOpenHistorial(factura)}><MailOutlineIcon /></IconButton></Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
@@ -102,6 +113,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
const [filtroNombre, setFiltroNombre] = useState('');
|
||||
const [filtroEstadoPago, setFiltroEstadoPago] = useState('');
|
||||
const [filtroEstadoFacturacion, setFiltroEstadoFacturacion] = useState('');
|
||||
const [filtroTipoFactura, setFiltroTipoFactura] = useState('');
|
||||
|
||||
const [selectedFactura, setSelectedFactura] = useState<FacturaConsolidadaDto | null>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
@@ -121,7 +133,8 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
selectedMes,
|
||||
filtroNombre || undefined,
|
||||
filtroEstadoPago || undefined,
|
||||
filtroEstadoFacturacion || undefined
|
||||
filtroEstadoFacturacion || undefined,
|
||||
filtroTipoFactura || undefined
|
||||
);
|
||||
setResumenes(data);
|
||||
} catch (err) {
|
||||
@@ -130,7 +143,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedAnio, selectedMes, puedeConsultar, filtroNombre, filtroEstadoPago, filtroEstadoFacturacion]);
|
||||
}, [selectedAnio, selectedMes, puedeConsultar, filtroNombre, filtroEstadoPago, filtroEstadoFacturacion, filtroTipoFactura]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
@@ -218,6 +231,17 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
<TextField label="Buscar por Suscriptor" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} />
|
||||
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Pago</InputLabel><Select value={filtroEstadoPago} label="Estado de Pago" onChange={(e) => setFiltroEstadoPago(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosPago.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
|
||||
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Facturación</InputLabel><Select value={filtroEstadoFacturacion} label="Estado de Facturación" onChange={(e) => setFiltroEstadoFacturacion(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosFacturacion.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
|
||||
<FormControl sx={{ minWidth: 200 }} size="small">
|
||||
<InputLabel>Tipo de Factura</InputLabel>
|
||||
<Select
|
||||
value={filtroTipoFactura}
|
||||
label="Tipo de Factura"
|
||||
onChange={(e) => setFiltroTipoFactura(e.target.value)}
|
||||
>
|
||||
<MenuItem value=""><em>Todos</em></MenuItem>
|
||||
{tiposFactura.map(t => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
@@ -226,7 +250,7 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table aria-label="collapsible table">
|
||||
<TableHead><TableRow><TableCell /><TableCell>Suscriptor</TableCell><TableCell align="right">Saldo Total / Importe Total</TableCell><TableCell colSpan={5}></TableCell></TableRow></TableHead>
|
||||
<TableHead><TableRow><TableCell /><TableCell>Suscriptor</TableCell><TableCell align="right">Saldo Total / Importe Total</TableCell><TableCell colSpan={6}></TableCell></TableRow></TableHead>
|
||||
<TableBody>
|
||||
{loading ? (<TableRow><TableCell colSpan={8} align="center"><CircularProgress /></TableCell></TableRow>)
|
||||
: resumenes.length === 0 ? (<TableRow><TableCell colSpan={8} align="center">No hay facturas para el período seleccionado.</TableCell></TableRow>)
|
||||
@@ -252,22 +276,9 @@ const ConsultaFacturasPage: React.FC = () => {
|
||||
open={pagoModalOpen}
|
||||
onClose={handleClosePagoModal}
|
||||
onSubmit={handleSubmitPagoModal}
|
||||
factura={
|
||||
selectedFactura ? {
|
||||
idFactura: selectedFactura.idFactura,
|
||||
nombreSuscriptor: resumenes.find(r => r.idSuscriptor === resumenes.find(res => res.facturas.some(f => f.idFactura === selectedFactura.idFactura))?.idSuscriptor)?.nombreSuscriptor || '',
|
||||
importeFinal: selectedFactura.importeFinal,
|
||||
saldoPendiente: selectedFactura.estadoPago === 'Pagada' ? 0 : selectedFactura.importeFinal,
|
||||
idSuscriptor: resumenes.find(res => res.facturas.some(f => f.idFactura === selectedFactura.idFactura))?.idSuscriptor || 0,
|
||||
periodo: '',
|
||||
fechaEmision: '',
|
||||
fechaVencimiento: '',
|
||||
totalPagado: selectedFactura.importeFinal - (selectedFactura.estadoPago === 'Pagada' ? 0 : selectedFactura.importeFinal),
|
||||
estadoPago: selectedFactura.estadoPago,
|
||||
estadoFacturacion: selectedFactura.estadoFacturacion,
|
||||
numeroFactura: selectedFactura.numeroFactura,
|
||||
detalles: selectedFactura.detalles,
|
||||
} : null
|
||||
factura={selectedFactura}
|
||||
nombreSuscriptor={
|
||||
resumenes.find(r => r.idSuscriptor === selectedFactura?.idSuscriptor)?.nombreSuscriptor || ''
|
||||
}
|
||||
errorMessage={apiError}
|
||||
clearErrorMessage={() => setApiError(null)}
|
||||
|
||||
@@ -16,11 +16,12 @@ const SECCION_PERMISSIONS_PREFIX = "SS";
|
||||
|
||||
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
|
||||
if (codAcc === "SS001") return "Distribución";
|
||||
if (codAcc === "SS007") return "Suscripciones";
|
||||
if (codAcc === "SS002") return "Contables";
|
||||
if (codAcc === "SS003") return "Impresión";
|
||||
if (codAcc === "SS004") return "Reportes";
|
||||
if (codAcc === "SS005") return "Radios";
|
||||
if (codAcc === "SS006") return "Usuarios";
|
||||
if (codAcc === "SS005") return "Radios";
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -38,6 +39,9 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
moduloLower.includes("salidas otros destinos")) {
|
||||
return "Distribución";
|
||||
}
|
||||
if (moduloLower.includes("suscripciones")) {
|
||||
return "Suscripciones";
|
||||
}
|
||||
if (moduloLower.includes("cuentas pagos") ||
|
||||
moduloLower.includes("cuentas notas") ||
|
||||
moduloLower.includes("cuentas tipos pagos")) {
|
||||
@@ -50,9 +54,6 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
moduloLower.includes("tipos bobinas")) {
|
||||
return "Impresión";
|
||||
}
|
||||
if (moduloLower.includes("radios")) {
|
||||
return "Radios";
|
||||
}
|
||||
if (moduloLower.includes("usuarios") ||
|
||||
moduloLower.includes("perfiles")) {
|
||||
return "Usuarios";
|
||||
@@ -63,6 +64,9 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
|
||||
if (moduloLower.includes("permisos")) {
|
||||
return "Permisos (Definición)";
|
||||
}
|
||||
if (moduloLower.includes("radios")) {
|
||||
return "Radios";
|
||||
}
|
||||
return permisoModulo;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// src/routes/AppRoutes.tsx
|
||||
import React, { type JSX } from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
||||
import LoginPage from '../pages/LoginPage';
|
||||
import HomePage from '../pages/HomePage';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import MainLayout from '../layouts/MainLayout';
|
||||
import { Typography } from '@mui/material';
|
||||
import SectionProtectedRoute from './SectionProtectedRoute';
|
||||
|
||||
// Distribución
|
||||
@@ -267,7 +265,6 @@ const AppRoutes = () => {
|
||||
<ReportesIndexPage />
|
||||
</SectionProtectedRoute>}
|
||||
>
|
||||
<Route index element={<Typography sx={{ p: 2 }}>Seleccione un reporte del menú lateral.</Typography>} /> {/* Placeholder */}
|
||||
<Route path="existencia-papel" element={<ReporteExistenciaPapelPage />} />
|
||||
<Route path="movimiento-bobinas" element={<ReporteMovimientoBobinasPage />} />
|
||||
<Route path="movimiento-bobinas-estado" element={<ReporteMovimientoBobinasEstadoPage />} />
|
||||
|
||||
@@ -5,8 +5,10 @@ import type { UpdateDistribuidorDto } from '../../models/dtos/Distribucion/Updat
|
||||
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
|
||||
import type { DistribuidorLookupDto } from '../../models/dtos/Distribucion/DistribuidorLookupDto';
|
||||
|
||||
const getAllDistribuidores = async (nombreFilter?: string, nroDocFilter?: string): Promise<DistribuidorDto[]> => {
|
||||
const params: Record<string, string> = {};
|
||||
const getAllDistribuidores = async (nombreFilter?: string, nroDocFilter?: string, soloActivos: boolean = true): Promise<DistribuidorDto[]> => {
|
||||
const params: Record<string, string | boolean> = {
|
||||
soloActivos: soloActivos
|
||||
};
|
||||
if (nombreFilter) params.nombre = nombreFilter;
|
||||
if (nroDocFilter) params.nroDoc = nroDocFilter;
|
||||
|
||||
@@ -37,11 +39,15 @@ const deleteDistribuidor = async (id: number): Promise<void> => {
|
||||
await apiClient.delete(`/distribuidores/${id}`);
|
||||
};
|
||||
|
||||
const getAllDistribuidoresDropdown = async (): Promise<DistribuidorDropdownDto[]> => {
|
||||
const response = await apiClient.get<DistribuidorDropdownDto[]>('/distribuidores/dropdown');
|
||||
const getAllDistribuidoresDropdown = async (soloActivos: boolean = true): Promise<DistribuidorDropdownDto[]> => {
|
||||
const response = await apiClient.get<DistribuidorDropdownDto[]>('/distribuidores/dropdown', { params: { soloActivos } });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const toggleBajaDistribuidor = async (id: number, data: { darDeBaja: boolean, fechaBaja: string | null }): Promise<void> => {
|
||||
await apiClient.put(`/distribuidores/${id}/toggle-baja`, data);
|
||||
};
|
||||
|
||||
const distribuidorService = {
|
||||
getAllDistribuidores,
|
||||
getDistribuidorById,
|
||||
@@ -50,6 +56,7 @@ const distribuidorService = {
|
||||
deleteDistribuidor,
|
||||
getAllDistribuidoresDropdown,
|
||||
getDistribuidorLookupById,
|
||||
toggleBajaDistribuidor,
|
||||
};
|
||||
|
||||
export default distribuidorService;
|
||||
@@ -3,6 +3,8 @@ import type { StockBobinaDto } from '../../models/dtos/Impresion/StockBobinaDto'
|
||||
import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto';
|
||||
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
|
||||
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
|
||||
import type { CreateStockBobinaLoteDto } from '../../models/dtos/Impresion/CreateStockBobinaLoteDto';
|
||||
import type { UpdateFechaRemitoLoteDto } from '../../models/dtos/Impresion/UpdateFechaRemitoLoteDto';
|
||||
|
||||
interface GetAllStockBobinasParams {
|
||||
idTipoBobina?: number | null;
|
||||
@@ -12,6 +14,8 @@ interface GetAllStockBobinasParams {
|
||||
remitoFilter?: string | null;
|
||||
fechaDesde?: string | null; // "yyyy-MM-dd"
|
||||
fechaHasta?: string | null; // "yyyy-MM-dd"
|
||||
fechaEstadoDesde?: string | null; // "yyyy-MM-dd"
|
||||
fechaEstadoHasta?: string | null; // "yyyy-MM-dd"
|
||||
}
|
||||
|
||||
const getAllStockBobinas = async (filters: GetAllStockBobinasParams): Promise<StockBobinaDto[]> => {
|
||||
@@ -23,6 +27,8 @@ const getAllStockBobinas = async (filters: GetAllStockBobinasParams): Promise<St
|
||||
if (filters.remitoFilter) params.remito = filters.remitoFilter; // El backend espera remito
|
||||
if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde;
|
||||
if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta;
|
||||
if (filters.fechaEstadoDesde) params.fechaEstadoDesde = filters.fechaEstadoDesde;
|
||||
if (filters.fechaEstadoHasta) params.fechaEstadoHasta = filters.fechaEstadoHasta;
|
||||
|
||||
const response = await apiClient.get<StockBobinaDto[]>('/stockbobinas', { params });
|
||||
return response.data;
|
||||
@@ -50,6 +56,23 @@ const deleteIngresoBobina = async (idBobina: number): Promise<void> => {
|
||||
await apiClient.delete(`/stockbobinas/${idBobina}`);
|
||||
};
|
||||
|
||||
const verificarRemitoExistente = async (idPlanta: number, remito: string, fechaRemito?: string | null): Promise<StockBobinaDto[]> => {
|
||||
const params: { idPlanta: number; remito: string; fechaRemito?: string } = { idPlanta, remito };
|
||||
if (fechaRemito) {
|
||||
params.fechaRemito = fechaRemito;
|
||||
}
|
||||
const response = await apiClient.get<StockBobinaDto[]>('/stockbobinas/verificar-remito', { params });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const ingresarLoteBobinas = async (data: CreateStockBobinaLoteDto): Promise<void> => {
|
||||
await apiClient.post('/stockbobinas/lote', data);
|
||||
};
|
||||
|
||||
const actualizarFechaRemitoLote = async (data: UpdateFechaRemitoLoteDto): Promise<void> => {
|
||||
await apiClient.put('/stockbobinas/actualizar-fecha-remito', data);
|
||||
};
|
||||
|
||||
const stockBobinaService = {
|
||||
getAllStockBobinas,
|
||||
getStockBobinaById,
|
||||
@@ -57,6 +80,9 @@ const stockBobinaService = {
|
||||
updateDatosBobinaDisponible,
|
||||
cambiarEstadoBobina,
|
||||
deleteIngresoBobina,
|
||||
verificarRemitoExistente,
|
||||
ingresarLoteBobinas,
|
||||
actualizarFechaRemitoLote,
|
||||
};
|
||||
|
||||
export default stockBobinaService;
|
||||
@@ -20,6 +20,7 @@ import type { NovedadesCanillasReporteDto } from '../../models/dtos/Reportes/Nov
|
||||
import type { CanillaGananciaReporteDto } from '../../models/dtos/Reportes/CanillaGananciaReporteDto';
|
||||
import type { ListadoDistCanMensualDiariosDto } from '../../models/dtos/Reportes/ListadoDistCanMensualDiariosDto';
|
||||
import type { ListadoDistCanMensualPubDto } from '../../models/dtos/Reportes/ListadoDistCanMensualPubDto';
|
||||
import axios from 'axios';
|
||||
|
||||
interface GetExistenciaPapelParams {
|
||||
fechaDesde: string; // yyyy-MM-dd
|
||||
@@ -210,23 +211,42 @@ const getVentaMensualSecretariaTirDevoPdf = async (params: { fechaDesde: string;
|
||||
};
|
||||
|
||||
const getReporteDistribucionCanillas = async (params: {
|
||||
fecha: string;
|
||||
idEmpresa: number;
|
||||
fecha: string;
|
||||
idEmpresa: number;
|
||||
esAccionista?: boolean; // Hacerlo opcional
|
||||
}): Promise<ReporteDistribucionCanillasResponseDto> => {
|
||||
const response = await apiClient.get<ReporteDistribucionCanillasResponseDto>('/reportes/distribucion-canillas', { params });
|
||||
return response.data;
|
||||
try {
|
||||
const response = await apiClient.get<ReporteDistribucionCanillasResponseDto>('/reportes/distribucion-canillas', { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error al obtener datos del reporte de distribución de canillas:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getReporteDistribucionCanillasPdf = async (params: {
|
||||
fecha: string;
|
||||
idEmpresa: number;
|
||||
soloTotales: boolean; // Nuevo parámetro
|
||||
fecha: string;
|
||||
idEmpresa: number;
|
||||
esAccionista?: boolean; // Opcional
|
||||
soloTotales: boolean;
|
||||
}): Promise<Blob> => {
|
||||
const response = await apiClient.get('/reportes/distribucion-canillas/pdf', { // La ruta no necesita cambiar si el backend lo maneja
|
||||
params, // soloTotales se enviará como query param si el backend lo espera así
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
try {
|
||||
const response = await apiClient.get('/reportes/distribucion-canillas/pdf', {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error al generar PDF del reporte de distribución de canillas:', error);
|
||||
if (axios.isAxiosError(error) && error.response?.data) {
|
||||
// Si el error es un JSON dentro de un Blob, hay que leerlo
|
||||
if (error.response.data.type === 'application/json') {
|
||||
const errorJson = JSON.parse(await error.response.data.text());
|
||||
throw new Error(errorJson.message || "Error al generar PDF");
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getTiradasPublicacionesSecciones = async (params: {
|
||||
|
||||
@@ -19,11 +19,16 @@ const procesarArchivoRespuesta = async (archivo: File): Promise<ProcesamientoLot
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getResumenesDeCuentaPorPeriodo = async (anio: number, mes: number, nombreSuscriptor?: string, estadoPago?: string, estadoFacturacion?: string): Promise<ResumenCuentaSuscriptorDto[]> => {
|
||||
const getResumenesDeCuentaPorPeriodo = async (
|
||||
anio: number, mes: number,
|
||||
nombreSuscriptor?: string, estadoPago?: string, estadoFacturacion?: string,
|
||||
tipoFactura?: string
|
||||
): Promise<ResumenCuentaSuscriptorDto[]> => {
|
||||
const params = new URLSearchParams();
|
||||
if (nombreSuscriptor) params.append('nombreSuscriptor', nombreSuscriptor);
|
||||
if (estadoPago) params.append('estadoPago', estadoPago);
|
||||
if (estadoFacturacion) params.append('estadoFacturacion', estadoFacturacion);
|
||||
if (tipoFactura) params.append('tipoFactura', tipoFactura);
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `${API_URL}/${anio}/${mes}${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
@@ -33,19 +33,18 @@ apiClient.interceptors.response.use(
|
||||
(error) => {
|
||||
// Cualquier código de estado que este fuera del rango de 2xx causa la ejecución de esta función
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
if (error.response.status === 401) {
|
||||
// Token inválido o expirado
|
||||
console.warn("Error 401: Token inválido o expirado. Deslogueando...");
|
||||
// Verificamos si la petición fallida NO fue al endpoint de login.
|
||||
const isLoginAttempt = error.config?.url?.endsWith('/auth/login');
|
||||
|
||||
// Solo activamos el deslogueo automático si el error 401 NO es de un intento de login.
|
||||
if (error.response.status === 401 && !isLoginAttempt) {
|
||||
console.warn("Error 401 (Token inválido o expirado) detectado. Deslogueando...");
|
||||
|
||||
// Limpiar localStorage y recargar la página.
|
||||
// AuthContext se encargará de redirigir a /login al recargar porque no encontrará token.
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('authUser'); // Asegurar limpiar también el usuario
|
||||
// Forzar un hard refresh para que AuthContext se reinicialice y redirija
|
||||
// Esto también limpiará cualquier estado de React.
|
||||
// --- Mostrar mensaje antes de redirigir ---
|
||||
localStorage.removeItem('authUser');
|
||||
|
||||
alert("Tu sesión ha expirado o no es válida. Serás redirigido a la página de inicio de sesión.");
|
||||
window.location.href = '/login'; // Redirección más directa
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
// Es importante devolver el error para que el componente que hizo la llamada pueda manejarlo también si es necesario
|
||||
|
||||
20
README.md
20
README.md
@@ -1,8 +1,8 @@
|
||||
# Gestion Integral Web
|
||||
|
||||
**Gestion Integral Web** es un sistema de gestión empresarial, diseñado para administrar las operaciones de una empresa de medios de comunicación. Este proyecto representa la migración y modernización de un sistema de escritorio heredado, desarrollado originalmente en VB.NET, a una arquitectura web moderna y robusta.
|
||||
**Gestion Integral Web** es un sistema de gestión empresarial, diseñado para administrar las operaciones de una empresa de medios de comunicación. Este proyecto representa la migración y modernización de un sistema de escritorio heredado, desarrollado originalmente en VB.NET (Migrado de Cobol), a una arquitectura web moderna y robusta.
|
||||
|
||||
El sistema se compone de un **backend RESTful API desarrollado en ASP.NET Core** y un **frontend interactivo de tipo SPA (Single Page Application) construido con React y TypeScript**.
|
||||
El sistema se compone de un **backend APIRest desarrollado en ASP.NET Core** y un **frontend interactivo de tipo SPA (Single Page Application) construido con React y TypeScript**.
|
||||
|
||||
---
|
||||
## Módulos y Funcionalidades Principales
|
||||
@@ -60,6 +60,22 @@ El sistema está organizado en varios módulos clave para cubrir todas las área
|
||||
- **Autenticación Segura:** Mediante JSON Web Tokens (JWT).
|
||||
- **Auditoría:** Todas las modificaciones a los datos maestros y transacciones importantes se registran en tablas de historial (`_H`).
|
||||
|
||||
### 📨 Suscripciones
|
||||
- **Gestión de Suscriptores:** ABM completo de clientes, incluyendo datos de contacto, dirección de entrega y forma de pago preferida.
|
||||
- **Ciclo de Vida de la Suscripción:** Creación y administración de suscripciones por cliente y publicación, con fechas de inicio, fin, días de entrega y estados (`Activa`, `Pausada`, `Cancelada`).
|
||||
- **Facturación Proporcional (Pro-rata):** El sistema genera automáticamente una "Factura de Alta" por el monto proporcional cuando un cliente se suscribe en un período ya cerrado, evitando cobros excesivos en la primera factura.
|
||||
- **Gestión de Promociones:** ABM de promociones (ej. descuentos porcentuales, bonificación de días) y asignación a suscripciones específicas con vigencia definida.
|
||||
- **Cuenta Corriente del Suscriptor:** Administración de ajustes manuales (`Crédito`/`Débito`) para manejar situaciones excepcionales como notas de crédito, devoluciones o cargos especiales.
|
||||
- **Procesos de Cierre Mensual:**
|
||||
- **Generación de Cierre:** Proceso masivo que calcula y genera todas las facturas del período, aplicando promociones y ajustes.
|
||||
- **Notificaciones Automáticas:** Envío automático de resúmenes de cuenta por email a cada suscriptor al generar el cierre.
|
||||
- **Gestión de Débito Automático:**
|
||||
- **Generación de Archivo:** Creación del archivo de texto plano en formato "Pago Directo" para el Banco Galicia. Las "Facturas de Alta" se excluyen automáticamente de este proceso.
|
||||
- **Procesamiento de Respuesta:** Herramienta para procesar el archivo de respuesta del banco, actualizando los estados de pago (`Pagada`/`Rechazada`) de forma masiva.
|
||||
- **Auditoría de Comunicaciones:**
|
||||
- **Log de Envíos:** Registro detallado de cada correo electrónico enviado (individual o masivo), incluyendo estado (`Enviado`/`Fallido`) y mensajes de error.
|
||||
- **Historial de Envíos:** Interfaz para consultar el historial de notificaciones enviadas por cada factura o por cada lote de cierre mensual.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Stack Tecnológico
|
||||
|
||||
@@ -7,9 +7,14 @@ services:
|
||||
- shared-net
|
||||
environment:
|
||||
- DB_SA_PASSWORD=${DB_SA_PASSWORD}
|
||||
- ConnectionStrings__DefaultConnection=Server=db-sqlserver;Database=SistemaGestion;User ID=sa;Password=${DB_SA_PASSWORD};TrustServerCertificate=True;
|
||||
- ConnectionStrings__DefaultConnection=Server=db-sqlserver;Database=SistemaGestion;User ID=sa;Password=${DB_SA_PASSWORD};MultipleActiveResultSets=True;TrustServerCertificate=True;
|
||||
ports:
|
||||
- "8081:8080"
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# --- Servicio del Frontend ---
|
||||
web-gestion:
|
||||
@@ -21,6 +26,11 @@ services:
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- api-gestion
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
|
||||
networks:
|
||||
|
||||
Reference in New Issue
Block a user