9 Commits

Author SHA1 Message Date
7e274ef114 Actualizar README.md
Some checks failed
Optimized Build and Deploy / remote-build-and-deploy (push) Has been cancelled
2026-03-25 15:00:39 +00:00
5212e31a03 Feat: Baja Lógica de Distribuidores (Selectores Dropdown)
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 8m32s
2026-03-23 14:09:26 -03:00
9201d7222b Fix: Fechas de Estado Bobinas
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 14m8s
- Las fechas de estado de las bobinas no pueden ser anterior a la fecha de remito (ingreso).
2026-02-11 14:52:58 -03:00
fc27b4b43e feat(Reportes): Ajusta cálculo de promedios en Listado de Distribución
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 3m58s
Se modifica la lógica de cálculo para la fila "General" en la tabla de promedios del reporte de Listado de Distribución para distribuidores.

**Motivación:**
Por requerimiento explícito del usuario final, el cálculo de los promedios generales (Llevados, Devueltos, Ventas y % Devolución) debe ser un promedio aritmético simple de los valores de los días de la semana mostrados en la tabla (ej. Viernes, Sábado, Domingo), en lugar del promedio ponderado que se calculaba anteriormente basado en los totales generales.

**Cambios Realizados:**

1.  **Backend (`ListadoDistribucionDistribuidoresViewModel.cs`):**
    *   Se actualizó la propiedad `PromedioGeneral` para que calcule sus valores (Promedio\_Llevados, Promedio\_Devueltos, etc.) promediando directamente los valores de la colección `PromediosPorDia`.

2.  **PDF (`ListadoDistribucionDistribuidoresDocument.cs`):**
    *   Se ajustó la lógica de renderizado de la fila "General" para que el porcentaje de devolución también se calcule como el promedio de los porcentajes de los días individuales, asegurando consistencia con el ViewModel.

3.  **Frontend (`ReporteListadoDistribucionPage.tsx`):**
    *   Se modificó el cálculo del estado `totalesPromedios` dentro de la función `handleGenerarReporte`. Ahora, en lugar de usar los totales de la tabla de detalle, suma los valores de la tabla de promedios y los divide por la cantidad de días para obtener un promedio simple.

**Resultado:**
Tanto la interfaz web como el PDF generado ahora muestran en la fila "General" un promedio simple de las filas de promedios diarios, alineándose con la lógica solicitada por el usuario.
2025-12-05 12:25:18 -03:00
35e8d803b9 Fix: Alinea cálculos del PDF y web en distribución de canillitas
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 4m16s
Este commit soluciona varias inconsistencias de cálculo y visualización que existían entre el reporte de distribución para canillitas en la web y el PDF generado.

Los cambios principales incluyen:

- **Cálculo de Promedios Generales:** Se implementa un promedio ponderado utilizando división decimal y redondeo matemático (`MidpointRounding.AwayFromZero`) en el backend. Esto soluciona las diferencias numéricas en los totales ("Prom. Llevados", "Prom. Devueltos", etc.) que eran causadas por la división de enteros.

- **Corrección de % Devolución:** Se ajusta la fórmula en todo el reporte (web y PDF) para calcular correctamente el porcentaje de devolución (`Devueltos / Llevados`) en lugar del porcentaje de venta que se mostraba erróneamente.

- **Orden de Columnas en PDF:** Se corrige el orden de las columnas "Prom. Devueltos" y "Prom. Ventas" que estaban intercambiadas en la fila "General" del PDF.

- **Precisión en Redondeo Final:** Se refina el cálculo del "% Devolución General" para que se base en los totales sin redondear, eliminando una diferencia de 0.01% y logrando una paridad exacta con la interfaz web.
2025-12-04 10:19:36 -03:00
8e1b8d2326 feat: DataGrid y filtro por Fechas en Stock Bobinas
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 2m15s
Frontend:
- Se reemplazó el componente Table por DataGrid para habilitar ordenamiento y filtrado nativo en cliente.
- Se agregó la UI para filtrar por rango de "Fecha de Estado".
- Se corrigió el tipado de columnas de fecha (`type: 'date'`) implementando un `valueGetter` personalizado que parsea año/mes/día localmente para evitar errores de filtrado por diferencia de Zona Horaria (UTC vs Local).
- Se actualizó `stockBobinaService` para enviar los parámetros `fechaEstadoDesde` y `fechaEstadoHasta`.

Backend:
- Se actualizó `StockBobinasController` para recibir los nuevos parámetros de fecha.
- Se modificó `StockBobinaRepository` implementando la lógica SQL para los nuevos filtros.
2025-11-27 13:49:46 -03:00
bc19e184aa feat: Implementar ingreso de bobinas por lote
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 2m13s
Se introduce una nueva funcionalidad para el ingreso masivo de bobinas a partir de un único remito. Esto agiliza significativamente la carga de datos y reduce errores al evitar la repetición de la planta, número y fecha de remito.

La implementación incluye:
- Un modal maestro-detalle de dos pasos que primero verifica el remito y luego permite la carga de las bobinas.
- Lógica de autocompletado de fecha y feedback al usuario si el remito ya existe.
- Un nuevo endpoint en el backend para procesar el lote de forma transaccional.
2025-11-20 09:50:54 -03:00
29109cff13 Fix Se deshabilita verificación de remito
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 3m41s
Pedido por Claudia Acosta:
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.
2025-11-18 13:14:24 -03:00
7f1fadfc84 Feat Se Añade Total a Canillas Page
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 1m48s
2025-11-10 15:07:36 -03:00
43 changed files with 1837 additions and 638 deletions

View 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';

View File

@@ -40,19 +40,19 @@ namespace GestionIntegral.Api.Controllers.Distribucion
[HttpGet] [HttpGet]
[ProducesResponseType(typeof(IEnumerable<DistribuidorDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(IEnumerable<DistribuidorDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [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(); if (!TienePermiso(PermisoVer)) return Forbid();
var distribuidores = await _distribuidorService.ObtenerTodosAsync(nombre, nroDoc); var distribuidores = await _distribuidorService.ObtenerTodosAsync(nombre, nroDoc, soloActivos);
return Ok(distribuidores); return Ok(distribuidores);
} }
[HttpGet("dropdown")] [HttpGet("dropdown")]
[ProducesResponseType(typeof(IEnumerable<DistribuidorDropdownDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(IEnumerable<DistribuidorDropdownDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [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); return Ok(distribuidores);
} }
@@ -117,6 +117,27 @@ namespace GestionIntegral.Api.Controllers.Distribucion
return NoContent(); 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}")] [HttpDelete("{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]

View File

@@ -2,6 +2,7 @@ using GestionIntegral.Api.Dtos.Impresion;
using GestionIntegral.Api.Services.Impresion; using GestionIntegral.Api.Services.Impresion;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -40,6 +41,7 @@ namespace GestionIntegral.Api.Controllers.Impresion
return null; return null;
} }
// GET: api/stockbobinas
// GET: api/stockbobinas // GET: api/stockbobinas
[HttpGet] [HttpGet]
[ProducesResponseType(typeof(IEnumerable<StockBobinaDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(IEnumerable<StockBobinaDto>), StatusCodes.Status200OK)]
@@ -47,12 +49,23 @@ namespace GestionIntegral.Api.Controllers.Impresion
public async Task<IActionResult> GetAllStockBobinas( public async Task<IActionResult> GetAllStockBobinas(
[FromQuery] int? idTipoBobina, [FromQuery] string? nroBobina, [FromQuery] int? idPlanta, [FromQuery] int? idTipoBobina, [FromQuery] string? nroBobina, [FromQuery] int? idPlanta,
[FromQuery] int? idEstadoBobina, [FromQuery] string? remito, [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(); if (!TienePermiso(PermisoVerStock)) return Forbid();
try 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); return Ok(bobinas);
} }
catch (Exception ex) catch (Exception ex)
@@ -127,7 +140,7 @@ namespace GestionIntegral.Api.Controllers.Impresion
if (!ModelState.IsValid) return BadRequest(ModelState); // Validaciones de DTO (Required, Range, etc.) if (!ModelState.IsValid) return BadRequest(ModelState); // Validaciones de DTO (Required, Range, etc.)
var userId = GetCurrentUserId(); var userId = GetCurrentUserId();
if (userId == null) return Unauthorized(); if (userId == null) return Unauthorized();
// La validación de que IdPublicacion/IdSeccion son requeridos para estado "En Uso" // La validación de que IdPublicacion/IdSeccion son requeridos para estado "En Uso"
// ahora está más robusta en el servicio. Se puede quitar del controlador // ahora está más robusta en el servicio. Se puede quitar del controlador
// si se prefiere que el servicio sea la única fuente de verdad para esa lógica. // si se prefiere que el servicio sea la única fuente de verdad para esa lógica.
@@ -172,5 +185,72 @@ namespace GestionIntegral.Api.Controllers.Impresion
} }
return NoContent(); 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.
}
} }
} }

View File

@@ -148,7 +148,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
foreach (var item in Model.PromediosPorDia.OrderBy(d => dayOrder.GetValueOrDefault(d.Dia, 99))) 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).Text(item.Dia);
table.Cell().Border(1).Padding(3).AlignRight().Text(item.Cant.ToString("N0")); 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; var general = Model.PromedioGeneral;
if (general != null) if (general != null)
{ {
var porcDevolucionGeneral = general.Promedio_Llevados > 0 ? (decimal)general.Promedio_Devueltos * 100 / general.Promedio_Llevados : 0;
var boldStyle = TextStyle.Default.SemiBold(); var boldStyle = TextStyle.Default.SemiBold();
table.Cell().Border(1).Padding(3).Text(text => text.Span(general.Dia).Style(boldStyle)); 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_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_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(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));
} }
}); });
}); });

View File

@@ -24,7 +24,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
{ {
page.Margin(1, Unit.Centimetre); page.Margin(1, Unit.Centimetre);
page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(10)); page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(10));
page.Header().Element(ComposeHeader); page.Header().Element(ComposeHeader);
page.Content().Element(ComposeContent); page.Content().Element(ComposeContent);
page.Footer().AlignCenter().Text(x => { x.Span("Página "); x.CurrentPageNumber(); }); page.Footer().AlignCenter().Text(x => { x.Span("Página "); x.CurrentPageNumber(); });
@@ -46,7 +46,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
}); });
}); });
} }
void ComposeContent(IContainer container) void ComposeContent(IContainer container)
{ {
container.PaddingTop(8, Unit.Millimetre).Column(column => container.PaddingTop(8, Unit.Millimetre).Column(column =>
@@ -54,7 +54,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
column.Spacing(15); column.Spacing(15);
column.Item().Text("Distribución").SemiBold().FontSize(12); column.Item().Text("Distribución").SemiBold().FontSize(12);
column.Item().Element(ComposeDetalleDiarioTable); column.Item().Element(ComposeDetalleDiarioTable);
column.Item().PaddingTop(5, Unit.Millimetre); column.Item().PaddingTop(5, Unit.Millimetre);
column.Item().Text("Promedios").SemiBold().FontSize(12); column.Item().Text("Promedios").SemiBold().FontSize(12);
@@ -90,14 +90,14 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
var llevados = item.Llevados ?? 0; var llevados = item.Llevados ?? 0;
var devueltos = item.Devueltos ?? 0; var devueltos = item.Devueltos ?? 0;
var ventaNetaDia = llevados - devueltos; var ventaNetaDia = llevados - devueltos;
if(llevados > 0) if (llevados > 0)
{ {
ventaNetaAcumulada += ventaNetaDia; ventaNetaAcumulada += ventaNetaDia;
conteoDias++; conteoDias++;
} }
var promedio = conteoDias > 0 ? ventaNetaAcumulada / conteoDias : 0; var promedio = conteoDias > 0 ? ventaNetaAcumulada / conteoDias : 0;
var porcDevolucion = llevados > 0 ? (decimal)devueltos * 100 / llevados : 0; var porcDevolucion = llevados > 0 ? (decimal)devueltos * 100 / llevados : 0;
table.Cell().Border(1).Padding(3).Text(item.Dia.ToString()); table.Cell().Border(1).Padding(3).Text(item.Dia.ToString());
table.Cell().Border(1).Padding(3).AlignRight().Text(llevados.ToString("N0")); table.Cell().Border(1).Padding(3).AlignRight().Text(llevados.ToString("N0"));
table.Cell().Border(1).Padding(3).AlignRight().Text(devueltos.ToString("N0")); table.Cell().Border(1).Padding(3).AlignRight().Text(devueltos.ToString("N0"));
@@ -105,12 +105,12 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
table.Cell().Border(1).Padding(3).AlignRight().Text(promedio.ToString("N0")); table.Cell().Border(1).Padding(3).AlignRight().Text(promedio.ToString("N0"));
table.Cell().Border(1).Padding(3).AlignRight().Text(porcDevolucion.ToString("F2") + "%"); table.Cell().Border(1).Padding(3).AlignRight().Text(porcDevolucion.ToString("F2") + "%");
} }
var totalLlevados = Model.DetalleDiario.Sum(i => i.Llevados ?? 0); var totalLlevados = Model.DetalleDiario.Sum(i => i.Llevados ?? 0);
var totalDevueltos = Model.DetalleDiario.Sum(i => i.Devueltos ?? 0); var totalDevueltos = Model.DetalleDiario.Sum(i => i.Devueltos ?? 0);
var totalVentaNeta = totalLlevados - totalDevueltos; var totalVentaNeta = totalLlevados - totalDevueltos;
var totalPorcDevolucion = totalLlevados > 0 ? (decimal)totalDevueltos * 100 / totalLlevados : 0; var totalPorcDevolucion = totalLlevados > 0 ? (decimal)totalDevueltos * 100 / totalLlevados : 0;
var boldStyle = TextStyle.Default.SemiBold(); var boldStyle = TextStyle.Default.SemiBold();
table.Cell().Border(1).Padding(3); table.Cell().Border(1).Padding(3);
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(totalLlevados.ToString("N0")).Style(boldStyle)); table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(totalLlevados.ToString("N0")).Style(boldStyle));
@@ -120,11 +120,11 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(totalPorcDevolucion.ToString("F2") + "%").Style(boldStyle)); table.Cell().Border(1).Padding(3).AlignRight().Text(t => t.Span(totalPorcDevolucion.ToString("F2") + "%").Style(boldStyle));
}); });
} }
void ComposePromediosTable(IContainer container) 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 => container.Table(table =>
{ {
table.ColumnsDefinition(columns => table.ColumnsDefinition(columns =>
@@ -147,7 +147,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
var llevados = item.Promedio_Llevados ?? 0; var llevados = item.Promedio_Llevados ?? 0;
var devueltos = item.Promedio_Devueltos ?? 0; var devueltos = item.Promedio_Devueltos ?? 0;
var porcDevolucion = llevados > 0 ? (decimal)devueltos * 100 / llevados : 0; var porcDevolucion = llevados > 0 ? (decimal)devueltos * 100 / llevados : 0;
table.Cell().Border(1).Padding(3).Text(item.Dia); table.Cell().Border(1).Padding(3).Text(item.Dia);
table.Cell().Border(1).Padding(3).AlignRight().Text(item.Cant?.ToString("N0") ?? "0"); table.Cell().Border(1).Padding(3).AlignRight().Text(item.Cant?.ToString("N0") ?? "0");
table.Cell().Border(1).Padding(3).AlignRight().Text(llevados.ToString("N0")); table.Cell().Border(1).Padding(3).AlignRight().Text(llevados.ToString("N0"));
@@ -161,16 +161,16 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
if (general != null) if (general != null)
{ {
var boldStyle = TextStyle.Default.SemiBold(); var boldStyle = TextStyle.Default.SemiBold();
var llevadosGeneral = general.Llevados ?? 0; // Usamos el total para el % var avgPercentage = Model.PromediosPorDia
var devueltosGeneral = general.Devueltos ?? 0; // Usamos el total para el % .Where(p => p.Dia != "General" && (p.Promedio_Llevados ?? 0) > 0)
var porcDevolucionGeneral = llevadosGeneral > 0 ? (decimal)devueltosGeneral * 100 / llevadosGeneral : 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).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.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_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_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(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));
} }
}); });
} }

View File

@@ -23,7 +23,7 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
public async Task<IEnumerable<int>> GetAllDistribuidorIdsAsync() 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 try
{ {
using (var connection = _connectionFactory.CreateConnection()) 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) 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(); var parameters = new DynamicParameters();
if (!string.IsNullOrWhiteSpace(destinoFilter)) if (!string.IsNullOrWhiteSpace(destinoFilter))
{ {
sqlBuilder.Append(" AND Destino = @Destino"); sqlBuilder.Append(" AND s.Destino = @Destino");
parameters.Add("Destino", destinoFilter); 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) if (idDestinoFilter.HasValue)
{ {
sqlBuilder.Append(" AND Id_Destino = @IdDestino"); sqlBuilder.Append(" AND s.Id_Destino = @IdDestino");
parameters.Add("IdDestino", idDestinoFilter.Value); parameters.Add("IdDestino", idDestinoFilter.Value);
} }
if (idEmpresaFilter.HasValue) if (idEmpresaFilter.HasValue)
{ {
sqlBuilder.Append(" AND Id_Empresa = @IdEmpresa"); sqlBuilder.Append(" AND s.Id_Empresa = @IdEmpresa");
parameters.Add("IdEmpresa", idEmpresaFilter.Value); 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(); using var connection = _connectionFactory.CreateConnection();
return await connection.QueryAsync<Saldo>(sqlBuilder.ToString(), parameters); return await connection.QueryAsync<Saldo>(sqlBuilder.ToString(), parameters);

View File

@@ -22,12 +22,12 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
_logger = logger; _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(@" var sqlBuilder = new StringBuilder(@"
SELECT SELECT
d.Id_Distribuidor AS IdDistribuidor, d.Nombre, d.Contacto, d.NroDoc, d.Id_Zona AS IdZona, 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 z.Nombre AS NombreZona
FROM dbo.dist_dtDistribuidores d FROM dbo.dist_dtDistribuidores d
LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona 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"); sqlBuilder.Append(" AND d.NroDoc LIKE @NroDocParam");
parameters.Add("NroDocParam", $"%{nroDocFilter}%"); parameters.Add("NroDocParam", $"%{nroDocFilter}%");
} }
if (soloActivos.HasValue)
{
sqlBuilder.Append(" AND d.Baja = @BajaStatus ");
parameters.Add("BajaStatus", !soloActivos.Value);
}
sqlBuilder.Append(" ORDER BY d.Nombre;"); sqlBuilder.Append(" ORDER BY d.Nombre;");
try 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(@" var sqlBuilder = new StringBuilder(@"
SELECT SELECT
@@ -71,6 +76,13 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
FROM dbo.dist_dtDistribuidores FROM dbo.dist_dtDistribuidores
WHERE 1=1"); WHERE 1=1");
var parameters = new DynamicParameters(); var parameters = new DynamicParameters();
if (soloActivos.HasValue)
{
sqlBuilder.Append(" AND Baja = @BajaStatus ");
parameters.Add("BajaStatus", !soloActivos.Value);
}
sqlBuilder.Append(" ORDER BY Nombre;"); sqlBuilder.Append(" ORDER BY Nombre;");
try try
{ {
@@ -92,7 +104,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
const string sql = @" const string sql = @"
SELECT SELECT
d.Id_Distribuidor AS IdDistribuidor, d.Nombre, d.Contacto, d.NroDoc, d.Id_Zona AS IdZona, 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 z.Nombre AS NombreZona
FROM dbo.dist_dtDistribuidores d FROM dbo.dist_dtDistribuidores d
LEFT JOIN dbo.dist_dtZonas z ON d.Id_Zona = z.Id_Zona 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 = @" const string sql = @"
SELECT SELECT
Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona, 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 FROM dbo.dist_dtDistribuidores
WHERE Id_Distribuidor = @IdParam"; WHERE Id_Distribuidor = @IdParam";
try try
@@ -223,10 +235,10 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
public async Task<Distribuidor?> CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction) public async Task<Distribuidor?> CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction)
{ {
const string sqlInsert = @" 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, 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 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);"; VALUES (@Nombre, @Contacto, @NroDoc, @IdZona, @Calle, @Numero, @Piso, @Depto, @Telefono, @Email, @Localidad, 0, NULL);";
var connection = transaction.Connection!; var connection = transaction.Connection!;
var inserted = await connection.QuerySingleAsync<Distribuidor>(sqlInsert, nuevoDistribuidor, transaction); var inserted = await connection.QuerySingleAsync<Distribuidor>(sqlInsert, nuevoDistribuidor, transaction);
@@ -234,8 +246,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
const string sqlInsertHistorico = @" const string sqlInsertHistorico = @"
INSERT INTO dbo.dist_dtDistribuidores_H INSERT INTO dbo.dist_dtDistribuidores_H
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod) (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, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
await connection.ExecuteAsync(sqlInsertHistorico, new await connection.ExecuteAsync(sqlInsertHistorico, new
{ {
@@ -251,6 +263,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
TelefonoParam = inserted.Telefono, TelefonoParam = inserted.Telefono,
EmailParam = inserted.Email, EmailParam = inserted.Email,
LocalidadParam = inserted.Localidad, LocalidadParam = inserted.Localidad,
BajaParam = inserted.Baja,
FechaBajaParam = inserted.FechaBaja,
IdUsuarioParam = idUsuario, IdUsuarioParam = idUsuario,
FechaModParam = DateTime.Now, FechaModParam = DateTime.Now,
TipoModParam = "Creado" TipoModParam = "Creado"
@@ -263,7 +277,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
var connection = transaction.Connection!; var connection = transaction.Connection!;
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>( var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona, @"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", FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdDistribuidorParam",
new { IdDistribuidorParam = distribuidorAActualizar.IdDistribuidor }, transaction); new { IdDistribuidorParam = distribuidorAActualizar.IdDistribuidor }, transaction);
if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado."); if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado.");
@@ -275,8 +289,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
WHERE Id_Distribuidor = @IdDistribuidor;"; WHERE Id_Distribuidor = @IdDistribuidor;";
const string sqlInsertHistorico = @" const string sqlInsertHistorico = @"
INSERT INTO dbo.dist_dtDistribuidores_H INSERT INTO dbo.dist_dtDistribuidores_H
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod) (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, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
await connection.ExecuteAsync(sqlInsertHistorico, new await connection.ExecuteAsync(sqlInsertHistorico, new
{ {
@@ -292,6 +306,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
TelefonoParam = actual.Telefono, TelefonoParam = actual.Telefono,
EmailParam = actual.Email, EmailParam = actual.Email,
LocalidadParam = actual.Localidad, LocalidadParam = actual.Localidad,
BajaParam = actual.Baja,
FechaBajaParam = actual.FechaBaja,
IdUsuarioParam = idUsuario, IdUsuarioParam = idUsuario,
FechaModParam = DateTime.Now, FechaModParam = DateTime.Now,
TipoModParam = "Actualizado" TipoModParam = "Actualizado"
@@ -306,7 +322,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
var connection = transaction.Connection!; var connection = transaction.Connection!;
var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>( var actual = await connection.QuerySingleOrDefaultAsync<Distribuidor>(
@"SELECT Id_Distribuidor AS IdDistribuidor, Nombre, Contacto, NroDoc, Id_Zona AS IdZona, @"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); FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdParam", new { IdParam = id }, transaction);
if (actual == null) throw new KeyNotFoundException("Distribuidor no encontrado."); 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 sqlDelete = "DELETE FROM dbo.dist_dtDistribuidores WHERE Id_Distribuidor = @IdParam";
const string sqlInsertHistorico = @" const string sqlInsertHistorico = @"
INSERT INTO dbo.dist_dtDistribuidores_H INSERT INTO dbo.dist_dtDistribuidores_H
(Id_Distribuidor, Nombre, Contacto, NroDoc, Id_Zona, Calle, Numero, Piso, Depto, Telefono, Email, Localidad, Id_Usuario, FechaMod, TipoMod) (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, @IdUsuarioParam, @FechaModParam, @TipoModParam);"; VALUES (@IdDistribuidorParam, @NombreParam, @ContactoParam, @NroDocParam, @IdZonaParam, @CalleParam, @NumeroParam, @PisoParam, @DeptoParam, @TelefonoParam, @EmailParam, @LocalidadParam, @BajaParam, @FechaBajaParam, @IdUsuarioParam, @FechaModParam, @TipoModParam);";
await connection.ExecuteAsync(sqlInsertHistorico, new await connection.ExecuteAsync(sqlInsertHistorico, new
{ {
@@ -331,6 +347,8 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
TelefonoParam = actual.Telefono, TelefonoParam = actual.Telefono,
EmailParam = actual.Email, EmailParam = actual.Email,
LocalidadParam = actual.Localidad, LocalidadParam = actual.Localidad,
BajaParam = actual.Baja,
FechaBajaParam = actual.FechaBaja,
IdUsuarioParam = idUsuario, IdUsuarioParam = idUsuario,
FechaModParam = DateTime.Now, FechaModParam = DateTime.Now,
TipoModParam = "Eliminado" TipoModParam = "Eliminado"
@@ -340,6 +358,47 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
return rowsAffected == 1; 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( public async Task<IEnumerable<(DistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaDesde, DateTime? fechaHasta,
int? idUsuarioModifico, string? tipoModificacion, int? idUsuarioModifico, string? tipoModificacion,

View File

@@ -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) 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"); 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 return true; // Asumir que existe en caso de error para prevenir duplicados
} }
} }
*/
public async Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction) public async Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction)
{ {

View File

@@ -8,16 +8,17 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
{ {
public interface IDistribuidorRepository 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? Distribuidor, string? NombreZona)> GetByIdAsync(int id);
Task<Distribuidor?> GetByIdSimpleAsync(int id); // Para uso interno en el servicio Task<Distribuidor?> GetByIdSimpleAsync(int id); // Para uso interno en el servicio
Task<Distribuidor?> CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction); Task<Distribuidor?> CreateAsync(Distribuidor nuevoDistribuidor, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(Distribuidor distribuidorAActualizar, int idUsuario, IDbTransaction transaction); Task<bool> UpdateAsync(Distribuidor distribuidorAActualizar, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteAsync(int id, 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> ExistsByNroDocAsync(string nroDoc, int? excludeIdDistribuidor = null);
Task<bool> ExistsByNameAsync(string nombre, 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<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<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
Task<IEnumerable<(DistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync( Task<IEnumerable<(DistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaDesde, DateTime? fechaHasta,

View File

@@ -13,7 +13,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction); Task<EntradaSalidaDist?> CreateAsync(EntradaSalidaDist nuevaES, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(EntradaSalidaDist esAActualizar, int idUsuario, IDbTransaction transaction); Task<bool> UpdateAsync(EntradaSalidaDist esAActualizar, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteAsync(int idParte, 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( Task<IEnumerable<(EntradaSalidaDistHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaDesde, DateTime? fechaHasta,
int? idUsuarioModifico, string? tipoModificacion, int? idUsuarioModifico, string? tipoModificacion,

View File

@@ -30,7 +30,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
d.Nombre AS NombreDistribuidor d.Nombre AS NombreDistribuidor
FROM dbo.dist_PorcPago pp FROM dbo.dist_PorcPago pp
INNER JOIN dbo.dist_dtDistribuidores d ON pp.Id_Distribuidor = d.Id_Distribuidor 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"; ORDER BY d.Nombre, pp.VigenciaD DESC";
try try
{ {

View File

@@ -15,7 +15,9 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
int? idEstadoBobina, int? idEstadoBobina,
string? remitoFilter, string? remitoFilter,
DateTime? fechaDesde, DateTime? fechaDesde,
DateTime? fechaHasta); DateTime? fechaHasta,
DateTime? fechaEstadoDesde,
DateTime? fechaEstadoHasta);
Task<StockBobina?> GetByIdAsync(int idBobina); Task<StockBobina?> GetByIdAsync(int idBobina);
Task<StockBobina?> GetByNroBobinaAsync(string nroBobina); // Para validar unicidad de NroBobina Task<StockBobina?> GetByNroBobinaAsync(string nroBobina); // Para validar unicidad de NroBobina

View File

@@ -23,7 +23,7 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
public async Task<IEnumerable<StockBobina>> GetAllAsync( public async Task<IEnumerable<StockBobina>> GetAllAsync(
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta, 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(@" var sqlBuilder = new StringBuilder(@"
SELECT SELECT
@@ -69,6 +69,16 @@ namespace GestionIntegral.Api.Data.Repositories.Distribucion
sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam"); sqlBuilder.Append(" AND sb.FechaRemito <= @FechaHastaParam");
parameters.Add("FechaHastaParam", fechaHasta.Value.Date); 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;"); sqlBuilder.Append(" ORDER BY sb.FechaRemito DESC, sb.NroBobina;");

View File

@@ -14,5 +14,7 @@ namespace GestionIntegral.Api.Models.Distribucion
public string? Telefono { get; set; } public string? Telefono { get; set; }
public string? Email { get; set; } public string? Email { get; set; }
public string? Localidad { 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)
} }
} }

View File

@@ -16,6 +16,8 @@ namespace GestionIntegral.Api.Models.Distribucion
public string? Telefono { get; set; } public string? Telefono { get; set; }
public string? Email { get; set; } public string? Email { get; set; }
public string? Localidad { get; set; } public string? Localidad { get; set; }
public bool? Baja { get; set; }
public DateTime? FechaBaja { get; set; }
public int Id_Usuario { get; set; } public int Id_Usuario { get; set; }
public DateTime FechaMod { get; set; } public DateTime FechaMod { get; set; }
public string TipoMod { get; set; } = string.Empty; public string TipoMod { get; set; } = string.Empty;

View File

@@ -15,5 +15,7 @@ namespace GestionIntegral.Api.Dtos.Distribucion
public string? Telefono { get; set; } public string? Telefono { get; set; }
public string? Email { get; set; } public string? Email { get; set; }
public string? Localidad { get; set; } public string? Localidad { get; set; }
public bool Baja { get; set; }
public DateTime? FechaBaja { get; set; }
} }
} }

View File

@@ -0,0 +1,10 @@
using System;
namespace GestionIntegral.Api.Dtos.Distribucion
{
public class ToggleBajaDistribuidorDto
{
public bool DarDeBaja { get; set; }
public DateTime? FechaBaja { get; set; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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();
}
}

View File

@@ -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; }
}
}

View File

@@ -13,7 +13,7 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
public string FechaDesde { get; set; } = string.Empty; public string FechaDesde { get; set; } = string.Empty;
public string FechaHasta { get; set; } = string.Empty; public string FechaHasta { get; set; } = string.Empty;
public string FechaReporte { get; set; } = DateTime.Now.ToString("dd/MM/yyyy"); public string FechaReporte { get; set; } = DateTime.Now.ToString("dd/MM/yyyy");
public ListadoDistribucionCanillasSimpleDto TotalesDetalleDiario public ListadoDistribucionCanillasSimpleDto TotalesDetalleDiario
{ {
get get
@@ -29,7 +29,23 @@ 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" --- // --- PROPIEDAD PARA LA FILA "GENERAL" ---
public ListadoDistribucionCanillasPromedioDiaDto? PromedioGeneral public ListadoDistribucionCanillasPromedioDiaDto? PromedioGeneral
{ {
@@ -37,20 +53,27 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
{ {
if (PromediosPorDia == null || !PromediosPorDia.Any()) return null; if (PromediosPorDia == null || !PromediosPorDia.Any()) return null;
// Sumamos los totales, no promediamos los promedios // Sumamos los totales ponderados para cada columna
var totalLlevados = PromediosPorDia.Sum(p => p.Llevados); var totalPonderadoLlevados = PromediosPorDia.Sum(p => p.Promedio_Llevados * p.Cant);
var totalDevueltos = PromediosPorDia.Sum(p => p.Devueltos); 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); var totalDias = PromediosPorDia.Sum(p => p.Cant);
if (totalDias == 0) return null; 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 return new ListadoDistribucionCanillasPromedioDiaDto
{ {
Dia = "General", Dia = "General",
Cant = totalDias, Cant = totalDias,
Promedio_Llevados = totalLlevados / totalDias, Promedio_Llevados = promGeneralLlevados,
Promedio_Devueltos = totalDevueltos / totalDias, Promedio_Devueltos = promGeneralDevueltos,
Promedio_Ventas = (totalLlevados - totalDevueltos) / totalDias Promedio_Ventas = promGeneralVentas
}; };
} }
} }

View File

@@ -20,26 +20,26 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
{ {
get get
{ {
if (DetalleDiario == null || !DetalleDiario.Any()) if (PromediosPorDia == null || !PromediosPorDia.Any())
{ {
return null; return null;
} }
var promediosValidos = PromediosPorDia.Where(p => p.Dia != "General").ToList();
var diasConDatos = DetalleDiario.Count(d => (d.Llevados ?? 0) > 0); if (!promediosValidos.Any()) return null;
if (diasConDatos == 0) return null; var countPromedios = promediosValidos.Count;
var sumPromLlevados = promediosValidos.Sum(p => p.Promedio_Llevados ?? 0);
var totalLlevados = DetalleDiario.Sum(d => d.Llevados ?? 0); var sumPromDevueltos = promediosValidos.Sum(p => p.Promedio_Devueltos ?? 0);
var totalDevueltos = DetalleDiario.Sum(d => d.Devueltos ?? 0); var sumPromVentas = promediosValidos.Sum(p => p.Promedio_Ventas ?? 0);
return new ListadoDistribucionDistPromedioDiaDto return new ListadoDistribucionDistPromedioDiaDto
{ {
Dia = "General", Dia = "General",
Cant = diasConDatos, Cant = promediosValidos.Sum(p => p.Cant ?? 0),
Promedio_Llevados = totalLlevados / diasConDatos, Promedio_Llevados = (int)Math.Round((decimal)sumPromLlevados / countPromedios, MidpointRounding.AwayFromZero),
Promedio_Devueltos = totalDevueltos / diasConDatos, Promedio_Devueltos = (int)Math.Round((decimal)sumPromDevueltos / countPromedios, MidpointRounding.AwayFromZero),
Promedio_Ventas = (totalLlevados - totalDevueltos) / diasConDatos, Promedio_Ventas = (int)Math.Round((decimal)sumPromVentas / countPromedios, MidpointRounding.AwayFromZero),
Llevados = totalLlevados, // Guardamos el total para el cálculo del % Llevados = (int)Math.Round((decimal)sumPromLlevados / countPromedios, MidpointRounding.AwayFromZero),
Devueltos = totalDevueltos // Guardamos el total para el cálculo del % Devueltos = (int)Math.Round((decimal)sumPromDevueltos / countPromedios, MidpointRounding.AwayFromZero)
}; };
} }
} }

View File

@@ -22,13 +22,16 @@ namespace GestionIntegral.Api.Services.Anomalia
public async Task<IEnumerable<AlertaGenericaDto>> ObtenerAlertasNoLeidasAsync() public async Task<IEnumerable<AlertaGenericaDto>> ObtenerAlertasNoLeidasAsync()
{ {
// Apunta a la nueva tabla genérica 'Sistema_Alertas' // 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 try
{ {
using (var connection = _dbConnectionFactory.CreateConnection()) using (var connection = _dbConnectionFactory.CreateConnection())
{ {
/*
var alertas = await connection.QueryAsync<AlertaGenericaDto>(query); var alertas = await connection.QueryAsync<AlertaGenericaDto>(query);
return alertas ?? Enumerable.Empty<AlertaGenericaDto>(); return alertas ?? Enumerable.Empty<AlertaGenericaDto>();
*/
return Enumerable.Empty<AlertaGenericaDto>();
} }
} }
catch (System.Exception ex) catch (System.Exception ex)
@@ -40,17 +43,20 @@ namespace GestionIntegral.Api.Services.Anomalia
public async Task<(bool Exito, string? Error)> MarcarComoLeidaAsync(int idAlerta) 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 try
{ {
using (var connection = _dbConnectionFactory.CreateConnection()) using (var connection = _dbConnectionFactory.CreateConnection())
{ {
/*
var result = await connection.ExecuteAsync(query, new { IdAlerta = idAlerta }); var result = await connection.ExecuteAsync(query, new { IdAlerta = idAlerta });
if (result > 0) if (result > 0)
{ {
return (true, null); return (true, null);
} }
return (false, "La alerta no fue encontrada o ya estaba marcada."); return (false, "La alerta no fue encontrada o ya estaba marcada.");
*/
return (true, null); // Retornar éxito silencioso por ahora
} }
} }
catch (System.Exception ex) 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) 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 try
{ {
using (var connection = _dbConnectionFactory.CreateConnection()) using (var connection = _dbConnectionFactory.CreateConnection())
{ {
/*
var result = await connection.ExecuteAsync(query, new { TipoAlerta = tipoAlerta, IdEntidad = idEntidad }); 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) // 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); _logger.LogInformation("Marcadas como leídas {Count} alertas para Tipo: {Tipo}, EntidadID: {IdEntidad}", result, tipoAlerta, idEntidad);
return (true, null); return (true, null);
*/
return (true, null);
} }
} }
catch (System.Exception ex) catch (System.Exception ex)

View File

@@ -56,20 +56,22 @@ namespace GestionIntegral.Api.Services.Distribucion
Depto = data.Distribuidor.Depto, Depto = data.Distribuidor.Depto,
Telefono = data.Distribuidor.Telefono, Telefono = data.Distribuidor.Telefono,
Email = data.Distribuidor.Email, 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 // 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!); 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 // Asegurar que el resultado no sea nulo y no contiene elementos nulos
if (data == null) 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( public async Task<IEnumerable<DistribuidorHistorialDto>> ObtenerHistorialAsync(
DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaDesde, DateTime? fechaHasta,
int? idUsuarioModifico, string? tipoModificacion, int? idUsuarioModifico, string? tipoModificacion,

View File

@@ -167,10 +167,11 @@ namespace GestionIntegral.Api.Services.Distribucion
var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(createDto.IdDistribuidor); var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(createDto.IdDistribuidor);
if (distribuidor == null) return (null, "Distribuidor no válido."); if (distribuidor == null) return (null, "Distribuidor no válido.");
/*
if (await _esRepository.ExistsByRemitoAndTipoForPublicacionAsync(createDto.Remito, createDto.TipoMovimiento, createDto.IdPublicacion)) 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."); 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 // Determinar IDs de Precio, Recargo y Porcentaje activos en la fecha del movimiento
var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(createDto.IdPublicacion, createDto.Fecha.Date); var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(createDto.IdPublicacion, createDto.Fecha.Date);

View File

@@ -7,12 +7,13 @@ namespace GestionIntegral.Api.Services.Distribucion
{ {
public interface IDistribuidorService 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?> ObtenerPorIdAsync(int id);
Task<(DistribuidorDto? Distribuidor, string? Error)> CrearAsync(CreateDistribuidorDto createDto, int idUsuario); 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)> ActualizarAsync(int id, UpdateDistribuidorDto updateDto, int idUsuario);
Task<(bool Exito, string? Error)> EliminarAsync(int id, 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<DistribuidorLookupDto?> ObtenerLookupPorIdAsync(int id);
Task<IEnumerable<DistribuidorHistorialDto>> ObtenerHistorialAsync( Task<IEnumerable<DistribuidorHistorialDto>> ObtenerHistorialAsync(
DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaDesde, DateTime? fechaHasta,

View File

@@ -10,7 +10,7 @@ namespace GestionIntegral.Api.Services.Impresion
{ {
Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync( Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta, 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?> ObtenerPorIdAsync(int idBobina);
Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario); Task<(StockBobinaDto? Bobina, string? Error)> IngresarBobinaAsync(CreateStockBobinaDto createDto, int idUsuario);
@@ -21,5 +21,8 @@ namespace GestionIntegral.Api.Services.Impresion
DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaDesde, DateTime? fechaHasta,
int? idUsuarioModifico, string? tipoModificacion, int? idUsuarioModifico, string? tipoModificacion,
int? idBobinaAfectada, int? idTipoBobinaFiltro, int? idPlantaFiltro, int? idEstadoBobinaFiltro); 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);
} }
} }

View File

@@ -85,9 +85,9 @@ namespace GestionIntegral.Api.Services.Impresion
public async Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync( public async Task<IEnumerable<StockBobinaDto>> ObtenerTodosAsync(
int? idTipoBobina, string? nroBobinaFilter, int? idPlanta, 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>(); var dtos = new List<StockBobinaDto>();
foreach (var bobina in bobinas) foreach (var bobina in bobinas)
{ {
@@ -166,16 +166,16 @@ namespace GestionIntegral.Api.Services.Impresion
} }
if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null) if (await _tipoBobinaRepository.GetByIdAsync(updateDto.IdTipoBobina) == null)
return (false, "Tipo de bobina inválido."); return (false, "Tipo de bobina inválido.");
if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null) //if (await _plantaRepository.GetByIdAsync(updateDto.IdPlanta) == null)
return (false, "Planta inválida."); // return (false, "Planta inválida.");
bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina; bobinaExistente.IdTipoBobina = updateDto.IdTipoBobina;
bobinaExistente.NroBobina = updateDto.NroBobina; bobinaExistente.NroBobina = updateDto.NroBobina;
bobinaExistente.Peso = updateDto.Peso; bobinaExistente.Peso = updateDto.Peso;
bobinaExistente.IdPlanta = updateDto.IdPlanta; //bobinaExistente.IdPlanta = updateDto.IdPlanta;
bobinaExistente.Remito = updateDto.Remito; //bobinaExistente.Remito = updateDto.Remito;
bobinaExistente.FechaRemito = updateDto.FechaRemito.Date; //bobinaExistente.FechaRemito = updateDto.FechaRemito.Date;
// FechaEstado se mantiene ya que el estado no cambia aquí // FechaEstado se mantiene ya que el estado no cambia aquí
var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados"); var actualizado = await _stockBobinaRepository.UpdateAsync(bobinaExistente, idUsuario, transaction, "Datos Actualizados");
@@ -199,12 +199,20 @@ namespace GestionIntegral.Api.Services.Impresion
using var transaction = connection.BeginTransaction(); using var transaction = connection.BeginTransaction();
try try
{ {
var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina); // Obtener dentro de la transacción var bobina = await _stockBobinaRepository.GetByIdAsync(idBobina);
if (bobina == null) if (bobina == null)
{ {
try { transaction.Rollback(); } catch { } try { transaction.Rollback(); } catch { }
return (false, "Bobina no encontrada."); 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); var nuevoEstado = await _estadoBobinaRepository.GetByIdAsync(cambiarEstadoDto.NuevoEstadoId);
if (nuevoEstado == null) if (nuevoEstado == null)
@@ -383,5 +391,153 @@ namespace GestionIntegral.Api.Services.Impresion
TipoMod = h.Historial.TipoMod TipoMod = h.Historial.TipoMod
}).ToList(); }).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}");
}
}
} }
} }

View File

@@ -7,7 +7,6 @@ import {
import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto'; import type { StockBobinaDto } from '../../../models/dtos/Impresion/StockBobinaDto';
import type { CambiarEstadoBobinaDto } from '../../../models/dtos/Impresion/CambiarEstadoBobinaDto'; import type { CambiarEstadoBobinaDto } from '../../../models/dtos/Impresion/CambiarEstadoBobinaDto';
import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto'; import type { EstadoBobinaDto } from '../../../models/dtos/Impresion/EstadoBobinaDto';
// --- CAMBIO: Importar PublicacionDropdownDto ---
import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto'; import type { PublicacionDropdownDto } from '../../../models/dtos/Distribucion/PublicacionDropdownDto';
import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto'; import type { PubliSeccionDto } from '../../../models/dtos/Distribucion/PubliSeccionDto';
import estadoBobinaService from '../../../services/Impresion/estadoBobinaService'; import estadoBobinaService from '../../../services/Impresion/estadoBobinaService';
@@ -33,272 +32,288 @@ const ID_ESTADO_EN_USO = 2; // Usaremos este consistentemente
const ID_ESTADO_DANADA = 3; const ID_ESTADO_DANADA = 3;
interface StockBobinaCambioEstadoModalProps { interface StockBobinaCambioEstadoModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise<void>; onSubmit: (idBobina: number, data: CambiarEstadoBobinaDto) => Promise<void>;
bobinaActual: StockBobinaDto | null; bobinaActual: StockBobinaDto | null;
errorMessage?: string | null; errorMessage?: string | null;
clearErrorMessage: () => void; clearErrorMessage: () => void;
} }
const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> = ({ const StockBobinaCambioEstadoModal: React.FC<StockBobinaCambioEstadoModalProps> = ({
open, open,
onClose, onClose,
onSubmit, onSubmit,
bobinaActual, bobinaActual,
errorMessage, errorMessage,
clearErrorMessage clearErrorMessage
}) => { }) => {
const [nuevoEstadoId, setNuevoEstadoId] = useState<number | string>(''); const [nuevoEstadoId, setNuevoEstadoId] = useState<number | string>('');
const [idPublicacion, setIdPublicacion] = useState<number | string>(''); const [idPublicacion, setIdPublicacion] = useState<number | string>('');
const [idSeccion, setIdSeccion] = useState<number | string>(''); const [idSeccion, setIdSeccion] = useState<number | string>('');
const [obs, setObs] = useState(''); const [obs, setObs] = useState('');
const [fechaCambioEstado, setFechaCambioEstado] = useState(''); const [fechaCambioEstado, setFechaCambioEstado] = useState('');
const [estadosDisponibles, setEstadosDisponibles] = useState<EstadoBobinaDto[]>([]); const [estadosDisponibles, setEstadosDisponibles] = useState<EstadoBobinaDto[]>([]);
// --- CAMBIO: Usar PublicacionDropdownDto para el estado --- const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDropdownDto[]>([]);
const [publicacionesDisponibles, setPublicacionesDisponibles] = useState<PublicacionDropdownDto[]>([]); const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]);
const [seccionesDisponibles, setSeccionesDisponibles] = useState<PubliSeccionDto[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingDropdowns, setLoadingDropdowns] = useState(false); const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
useEffect(() => { useEffect(() => {
const fetchDropdownData = async () => { const fetchDropdownData = async () => {
if (!bobinaActual) return; 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) {
setLoadingDropdowns(true); setLoadingDropdowns(true);
try { try {
const data = await publiSeccionService.getSeccionesPorPublicacion(Number(idPublicacion), true); const todosLosEstados = await estadoBobinaService.getAllEstadosBobina();
setSeccionesDisponibles(data); 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) { } catch (error) {
console.error("Error al cargar secciones:", error); console.error("Error al cargar datos para dropdowns (Cambio Estado Bobina)", error);
setLocalErrors(prev => ({ ...prev, secciones: 'Error al cargar secciones.'})); setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar datos necesarios.' }));
setSeccionesDisponibles([]);
} finally { } finally {
setLoadingDropdowns(false); 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 { } else {
setSeccionesDisponibles([]); setSeccionesDisponibles([]); // Limpiar si no se cumplen condiciones
// No es necesario setIdSeccion('') aquí si el useEffect de nuevoEstadoId ya lo hace. }
}, [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" setLoading(true);
useEffect(() => { try {
if (Number(nuevoEstadoId) !== ID_ESTADO_EN_USO) { const esEnUso = Number(nuevoEstadoId) === ID_ESTADO_EN_USO;
setIdPublicacion(''); const dataToSubmit: CambiarEstadoBobinaDto = {
setIdSeccion(''); nuevoEstadoId: Number(nuevoEstadoId),
} idPublicacion: esEnUso && idPublicacion ? Number(idPublicacion) : null,
}, [nuevoEstadoId]); 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 => { return (
const errors: { [key: string]: string | null } = {}; <Modal open={open} onClose={onClose}>
if (!nuevoEstadoId) errors.nuevoEstadoId = 'Seleccione un nuevo estado.'; <Box sx={modalStyle}>
if (!fechaCambioEstado.trim()) errors.fechaCambioEstado = 'La fecha es obligatoria.'; <Typography variant="h6" component="h2" gutterBottom>
else if (!/^\d{4}-\d{2}-\d{2}$/.test(fechaCambioEstado)) errors.fechaCambioEstado = 'Formato de fecha inválido.'; Cambiar Estado de Bobina: {bobinaActual.nroBobina}
</Typography>
if (Number(nuevoEstadoId) === ID_ESTADO_EN_USO) { <Typography variant="body2" gutterBottom>
if (!idPublicacion) errors.idPublicacion = 'Seleccione una publicación.'; Estado Actual: <strong>{bobinaActual.nombreEstadoBobina}</strong>
if (!idSeccion) errors.idSeccion = 'Seleccione una sección.'; </Typography>
} <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
setLocalErrors(errors); <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
return Object.keys(errors).length === 0; <FormControl fullWidth margin="dense" error={!!localErrors.nuevoEstadoId} required>
}; <InputLabel id="nuevo-estado-select-label">Nuevo Estado</InputLabel>
<Select
const handleInputChange = (fieldName: string) => { labelId="nuevo-estado-select-label"
if (localErrors[fieldName]) setLocalErrors(prev => ({ ...prev, [fieldName]: null })); label="Nuevo Estado"
if (errorMessage) clearErrorMessage(); value={nuevoEstadoId}
// La lógica de limpieza de pub/secc se movió a un useEffect dedicado a nuevoEstadoId onChange={(e) => {
// y el de sección a un useEffect de idPublicacion setNuevoEstadoId(e.target.value as number | string);
if (fieldName === 'idPublicacion') { // Si cambia la publicación, resetear seccion handleInputChange('nuevoEstadoId');
setIdSeccion(''); }}
} disabled={loading || loadingDropdowns || estadosDisponibles.length === 0}
};
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}
> >
<MenuItem value="" disabled><em>Seleccione publicación</em></MenuItem> <MenuItem value="" disabled><em>Seleccione un estado</em></MenuItem>
{publicacionesDisponibles.map((p) => (<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>))} {estadosDisponibles.map((e) => (<MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>))}
</Select> </Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption">{localErrors.idPublicacion}</Typography>} {localErrors.nuevoEstadoId && <Typography color="error" variant="caption">{localErrors.nuevoEstadoId}</Typography>}
</FormControl> </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 {Number(nuevoEstadoId) === ID_ESTADO_EN_USO && (
onChange={(e) => {setFechaCambioEstado(e.target.value); handleInputChange('fechaCambioEstado');}} <>
margin="dense" fullWidth error={!!localErrors.fechaCambioEstado} helperText={localErrors.fechaCambioEstado || ''} <FormControl fullWidth margin="dense" error={!!localErrors.idPublicacion} required>
disabled={loading} InputLabelProps={{ shrink: true }} <InputLabel id="publicacion-estado-select-label">Publicación</InputLabel>
/> <Select labelId="publicacion-estado-select-label" label="Publicación" value={idPublicacion}
<TextField label="Observaciones (Opcional)" value={obs} onChange={(e) => { setIdPublicacion(e.target.value as number); handleInputChange('idPublicacion'); }}
onChange={(e) => setObs(e.target.value)} disabled={loading || loadingDropdowns || publicacionesDisponibles.length === 0}
margin="dense" fullWidth multiline rows={3} disabled={loading} >
/> <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> </Box>
</Modal>
{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>
);
}; };
export default StockBobinaCambioEstadoModal; export default StockBobinaCambioEstadoModal;

View File

@@ -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;

View File

@@ -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;

View File

@@ -12,4 +12,6 @@ export interface DistribuidorDto {
telefono?: string | null; telefono?: string | null;
email?: string | null; email?: string | null;
localidad?: string | null; localidad?: string | null;
baja?: boolean;
fechaBaja?: string | null;
} }

View File

@@ -0,0 +1,5 @@
export interface BobinaLoteDetalleDto {
idTipoBobina: number;
nroBobina: string;
peso: number;
}

View File

@@ -0,0 +1,8 @@
import type { BobinaLoteDetalleDto } from './BobinaLoteDetalleDto';
export interface CreateStockBobinaLoteDto {
idPlanta: number;
remito: string;
fechaRemito: string; // "yyyy-MM-dd"
bobinas: BobinaLoteDetalleDto[];
}

View File

@@ -0,0 +1,6 @@
export interface UpdateFechaRemitoLoteDto {
idPlanta: number;
remito: string;
fechaRemitoActual: string; // "yyyy-MM-dd"
nuevaFechaRemito: string; // "yyyy-MM-dd"
}

View File

@@ -1,14 +1,16 @@
// src/pages/Distribucion/GestionarDistribuidoresPage.tsx // src/pages/Distribucion/GestionarDistribuidoresPage.tsx
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { 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, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert CircularProgress, Alert, Chip, FormControlLabel, ListItemIcon, ListItemText
} from '@mui/material'; } from '@mui/material';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import TrashIcon from '@mui/icons-material/Delete'; import TrashIcon from '@mui/icons-material/Delete';
import MoreVertIcon from '@mui/icons-material/MoreVert'; 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 distribuidorService from '../../services/Distribucion/distribuidorService';
import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto';
import type { CreateDistribuidorDto } from '../../models/dtos/Distribucion/CreateDistribuidorDto'; import type { CreateDistribuidorDto } from '../../models/dtos/Distribucion/CreateDistribuidorDto';
@@ -24,6 +26,7 @@ const GestionarDistribuidoresPage: React.FC = () => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [filtroNombre, setFiltroNombre] = useState(''); const [filtroNombre, setFiltroNombre] = useState('');
const [filtroNroDoc, setFiltroNroDoc] = useState(''); const [filtroNroDoc, setFiltroNroDoc] = useState('');
const [filtroSoloActivos, setFiltroSoloActivos] = useState<boolean | undefined>(true);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [editingDistribuidor, setEditingDistribuidor] = useState<DistribuidorDto | null>(null); const [editingDistribuidor, setEditingDistribuidor] = useState<DistribuidorDto | null>(null);
@@ -49,12 +52,12 @@ const GestionarDistribuidoresPage: React.FC = () => {
} }
setLoading(true); setError(null); setApiErrorMessage(null); setLoading(true); setError(null); setApiErrorMessage(null);
try { try {
const data = await distribuidorService.getAllDistribuidores(filtroNombre, filtroNroDoc); const data = await distribuidorService.getAllDistribuidores(filtroNombre, filtroNroDoc, filtroSoloActivos);
setDistribuidores(data); setDistribuidores(data);
} catch (err) { } catch (err) {
console.error(err); setError('Error al cargar los distribuidores.'); console.error(err); setError('Error al cargar los distribuidores.');
} finally { setLoading(false); } } finally { setLoading(false); }
}, [filtroNombre, filtroNroDoc, puedeVer]); }, [filtroNombre, filtroNroDoc, filtroSoloActivos, puedeVer]);
useEffect(() => { cargarDistribuidores(); }, [cargarDistribuidores]); useEffect(() => { cargarDistribuidores(); }, [cargarDistribuidores]);
@@ -94,6 +97,21 @@ const GestionarDistribuidoresPage: React.FC = () => {
handleMenuClose(); 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) => { const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, distribuidor: DistribuidorDto) => {
setAnchorEl(event.currentTarget); setSelectedDistribuidorRow(distribuidor); setAnchorEl(event.currentTarget); setSelectedDistribuidorRow(distribuidor);
}; };
@@ -132,7 +150,17 @@ const GestionarDistribuidoresPage: React.FC = () => {
onChange={(e) => setFiltroNroDoc(e.target.value)} onChange={(e) => setFiltroNroDoc(e.target.value)}
sx={{ flexGrow: 1, minWidth: '200px' }} 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> </Box>
{puedeCrear && ( {puedeCrear && (
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>Agregar Distribuidor</Button> <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>Nombre</TableCell><TableCell>Nro. Doc.</TableCell>
<TableCell>Contacto</TableCell><TableCell>Zona</TableCell> <TableCell>Contacto</TableCell><TableCell>Zona</TableCell>
<TableCell>Teléfono</TableCell><TableCell>Localidad</TableCell> <TableCell>Teléfono</TableCell><TableCell>Localidad</TableCell>
<TableCell>Estado</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} {(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead> </TableRow></TableHead>
<TableBody> <TableBody>
@@ -157,10 +186,11 @@ const GestionarDistribuidoresPage: React.FC = () => {
<TableRow><TableCell colSpan={7} align="center">No se encontraron distribuidores.</TableCell></TableRow> <TableRow><TableCell colSpan={7} align="center">No se encontraron distribuidores.</TableCell></TableRow>
) : ( ) : (
displayData.map((d) => ( 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.nombre}</TableCell><TableCell>{d.nroDoc}</TableCell>
<TableCell>{d.contacto || '-'}</TableCell><TableCell>{d.nombreZona || '-'}</TableCell> <TableCell>{d.contacto || '-'}</TableCell><TableCell>{d.nombreZona || '-'}</TableCell>
<TableCell>{d.telefono || '-'}</TableCell><TableCell>{d.localidad || '-'}</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) && ( {(puedeModificar || puedeEliminar) && (
<TableCell align="right"> <TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, d)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton> <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}> <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && (<MenuItem onClick={() => { handleOpenModal(selectedDistribuidorRow!); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} />Modificar</MenuItem>)} {puedeModificar && selectedDistribuidorRow && (
{puedeEliminar && (<MenuItem onClick={() => handleDelete(selectedDistribuidorRow!.idDistribuidor)}><TrashIcon fontSize="small" sx={{ mr: 1 }} />Eliminar</MenuItem>)} <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>} {(!puedeModificar && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
</Menu> </Menu>

View File

@@ -88,7 +88,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
setLoadingFiltersDropdown(false); // Detiene el spinner de los filtros setLoadingFiltersDropdown(false); // Detiene el spinner de los filtros
return; return;
} }
setLoadingFiltersDropdown(true); setLoadingFiltersDropdown(true);
setError(null); setError(null);
try { try {
@@ -130,14 +130,14 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const cargarMovimientos = useCallback(async () => { const cargarMovimientos = useCallback(async () => {
if (!puedeVer) { if (!puedeVer) {
setError("No tiene permiso para ver esta sección."); setError("No tiene permiso para ver esta sección.");
setLoading(false); setLoading(false);
return; return;
} }
if (!filtroFecha || !filtroIdCanillitaSeleccionado) { if (!filtroFecha || !filtroIdCanillitaSeleccionado) {
if (loading) setLoading(false); if (loading) setLoading(false);
setMovimientos([]); setMovimientos([]);
return; return;
} }
setLoading(true); setError(null); setApiErrorMessage(null); setLoading(true); setError(null); setApiErrorMessage(null);
try { try {
@@ -164,37 +164,37 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
useEffect(() => { useEffect(() => {
if (filtroFecha && filtroIdCanillitaSeleccionado) { if (filtroFecha && filtroIdCanillitaSeleccionado) {
cargarMovimientos(); cargarMovimientos();
} else { } else {
setMovimientos([]); setMovimientos([]);
if (loading) setLoading(false); if (loading) setLoading(false);
} }
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]); }, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]);
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => { const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
if (!puedeCrear && !item) { if (!puedeCrear && !item) {
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos."); setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
return; return;
} }
if (item && !puedeModificar) { if (item && !puedeModificar) {
setApiErrorMessage("No tiene permiso para modificar movimientos."); setApiErrorMessage("No tiene permiso para modificar movimientos.");
return; return;
} }
if (item) { if (item) {
setEditingMovimiento(item); setEditingMovimiento(item);
setPrefillModalData(null); setPrefillModalData(null);
} else { } else {
const canillitaSeleccionado = destinatariosDropdown.find( const canillitaSeleccionado = destinatariosDropdown.find(
c => c.idCanilla === Number(filtroIdCanillitaSeleccionado) c => c.idCanilla === Number(filtroIdCanillitaSeleccionado)
); );
setEditingMovimiento(null); setEditingMovimiento(null);
setPrefillModalData({ setPrefillModalData({
fecha: filtroFecha, fecha: filtroFecha,
idCanilla: filtroIdCanillitaSeleccionado, idCanilla: filtroIdCanillitaSeleccionado,
nombreCanilla: canillitaSeleccionado?.nomApe, nombreCanilla: canillitaSeleccionado?.nomApe,
idPublicacion: filtroIdPublicacion idPublicacion: filtroIdPublicacion
}); });
} }
setApiErrorMessage(null); setApiErrorMessage(null);
setModalOpen(true); setModalOpen(true);
@@ -224,7 +224,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
}); });
}; };
const handleSelectAllForLiquidar = (event: React.ChangeEvent<HTMLInputElement>) => { 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)); const newSelectedIds = new Set(movimientos.filter(m => !m.liquidado).map(m => m.idParte));
setSelectedIdsParaLiquidar(newSelectedIds); setSelectedIdsParaLiquidar(newSelectedIds);
} else { } else {
@@ -248,17 +248,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z'); const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z');
let fechaMovimientoMasReciente: Date | null = null; let fechaMovimientoMasReciente: Date | null = null;
selectedIdsParaLiquidar.forEach(idParte => { selectedIdsParaLiquidar.forEach(idParte => {
const movimiento = movimientos.find(m => m.idParte === idParte); const movimiento = movimientos.find(m => m.idParte === idParte);
if (movimiento && movimiento.fecha) { if (movimiento && movimiento.fecha) {
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z'); const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z');
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) { if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) {
fechaMovimientoMasReciente = movFecha; fechaMovimientoMasReciente = movFecha;
}
} }
}
}); });
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) { 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'})}).`); 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; return;
} }
setApiErrorMessage(null); setApiErrorMessage(null);
setLoading(true); setLoading(true);
@@ -277,9 +277,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) { if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) {
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla); console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
await handleImprimirTicketLiquidacion( await handleImprimirTicketLiquidacion(
movimientoParaTicket.idCanilla, movimientoParaTicket.idCanilla,
movimientoParaTicket.fecha, movimientoParaTicket.fecha,
false false
); );
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) { } else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
console.log("Liquidación exitosa para accionista. No se genera ticket automáticamente."); console.log("Liquidación exitosa para accionista. No se genera ticket automáticamente.");
@@ -344,9 +344,13 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
}; };
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const totalARendirVisible = useMemo(() => const totalARendirVisible = useMemo(() =>
displayData.filter(m => !m.liquidado).reduce((sum, item) => sum + item.montoARendir, 0) 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) { if (!puedeVer) {
return ( return (
@@ -367,7 +371,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
<Paper sx={{ p: 2, mb: 2 }}> <Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography> <Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}> <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)} onChange={(e) => setFiltroFecha(e.target.value)}
InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }}
required required
@@ -398,9 +402,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
onChange={(e) => setFiltroIdCanillitaSeleccionado(e.target.value as number | string)} onChange={(e) => setFiltroIdCanillitaSeleccionado(e.target.value as number | string)}
> >
<MenuItem value=""><em>Seleccione uno</em></MenuItem> <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> </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>
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }} disabled={loadingFiltersDropdown}> <FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
@@ -411,15 +415,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
</Select> </Select>
</FormControl> </FormControl>
</Box> </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 && ( {puedeCrear && (
<Button <Button
variant="contained" variant="contained"
startIcon={<AddIcon />} startIcon={<AddIcon />}
onClick={() => handleOpenModal()} onClick={() => handleOpenModal()}
disabled={!filtroFecha || !filtroIdCanillitaSeleccionado} disabled={!filtroFecha || !filtroIdCanillitaSeleccionado}
> >
Registrar Movimiento Registrar Movimiento
</Button> </Button>
)} )}
{puedeLiquidar && numSelectedToLiquidate > 0 && movimientos.some(m => selectedIdsParaLiquidar.has(m.idParte) && !m.liquidado) && ( {puedeLiquidar && numSelectedToLiquidate > 0 && movimientos.some(m => selectedIdsParaLiquidar.has(m.idParte) && !m.liquidado) && (
@@ -429,32 +433,39 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
)} )}
</Box> </Box>
</Paper> </Paper>
{!filtroFecha && <Alert severity="info" sx={{my:1}}>Por favor, seleccione una fecha.</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>} {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>} {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>} {error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</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 && ( {!loading && movimientos.length > 0 && (
<Paper sx={{ p: 1.5, mb: 2, mt:1, backgroundColor: 'grey.100' }}> <Paper sx={{ p: 1.5, mb: 2, mt: 1, backgroundColor: 'grey.100' }}>
<Box sx={{display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}> <Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
<Typography variant="subtitle1" sx={{mr:2}}> <Typography variant="subtitle1" sx={{ mr: 1 }}>
Total a Liquidar: Total:
</Typography> </Typography>
<Typography variant="h6" sx={{fontWeight: 'bold', color: 'error.main'}}> <Typography variant="h6" sx={{ paddingRight: '5px', fontWeight: 'bold', color: 'text.main' }}>
{totalARendirVisible.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })} {montoARendirAll.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
</Typography> </Typography>
</Box> -
<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> </Paper>
)} )}
{!loading && !error && puedeVer && filtroFecha && filtroIdCanillitaSeleccionado && ( {!loading && !error && puedeVer && filtroFecha && filtroIdCanillitaSeleccionado && (
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table size="small"> <Table size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
{puedeLiquidar && ( {puedeLiquidar && (
@@ -545,10 +556,10 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
</TableContainer> </TableContainer>
)} )}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && selectedRow && !selectedRow.liquidado && ( {puedeModificar && selectedRow && !selectedRow.liquidado && (
<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>)}
{selectedRow && selectedRow.liquidado && puedeLiquidar && ( {selectedRow && selectedRow.liquidado && puedeLiquidar && (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
@@ -560,7 +571,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
selectedRow.canillaEsAccionista selectedRow.canillaEsAccionista
); );
} }
handleMenuClose(); handleMenuClose();
}} }}
disabled={loadingTicketPdf} disabled={loadingTicketPdf}
> >
@@ -572,9 +583,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
{selectedRow && ( {selectedRow && (
((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados)) ((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados))
) && ( ) && (
<MenuItem onClick={() => {if (selectedRow) handleDelete(selectedRow.idParte);}}> <MenuItem onClick={() => { if (selectedRow) handleDelete(selectedRow.idParte); }}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> <DeleteIcon fontSize="small" sx={{ mr: 1 }} />
Eliminar Eliminar
</MenuItem> </MenuItem>
)} )}
</Menu> </Menu>
@@ -589,7 +600,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
clearErrorMessage={() => setApiErrorMessage(null)} clearErrorMessage={() => setApiErrorMessage(null)}
/> />
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}> <Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
<DialogTitle>Confirmar Liquidación</DialogTitle> <DialogTitle>Confirmar Liquidación</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>

View File

@@ -1,9 +1,11 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip, Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination, Alert, FormControl, InputLabel, Select, FormControlLabel, Checkbox
CircularProgress, Alert, FormControl, InputLabel, Select, FormControlLabel, Checkbox
} from '@mui/material'; } 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 AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert'; import MoreVertIcon from '@mui/icons-material/MoreVert';
import EditIcon from '@mui/icons-material/Edit'; 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 SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear'; import ClearIcon from '@mui/icons-material/Clear';
import EditCalendarIcon from '@mui/icons-material/EditCalendar';
import stockBobinaService from '../../services/Impresion/stockBobinaService'; import stockBobinaService from '../../services/Impresion/stockBobinaService';
import tipoBobinaService from '../../services/Impresion/tipoBobinaService'; 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 { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; 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 StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal'; import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
import StockBobinaCambioEstadoModal from '../../components/Modals/Impresion/StockBobinaCambioEstadoModal'; import StockBobinaCambioEstadoModal from '../../components/Modals/Impresion/StockBobinaCambioEstadoModal';
import StockBobinaLoteFormModal from '../../components/Modals/Impresion/StockBobinaLoteFormModal';
import { usePermissions } from '../../hooks/usePermissions'; import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios'; import axios from 'axios';
@@ -38,20 +44,27 @@ const ID_ESTADO_DANADA = 3;
const GestionarStockBobinasPage: React.FC = () => { const GestionarStockBobinasPage: React.FC = () => {
const [stock, setStock] = useState<StockBobinaDto[]>([]); 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 [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = 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 [filtroTipoBobina, setFiltroTipoBobina] = useState<number | string>('');
const [filtroNroBobina, setFiltroNroBobina] = useState(''); const [filtroNroBobina, setFiltroNroBobina] = useState('');
const [filtroPlanta, setFiltroPlanta] = useState<number | string>(''); const [filtroPlanta, setFiltroPlanta] = useState<number | string>('');
const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>(''); const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>('');
const [filtroRemito, setFiltroRemito] = useState(''); 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 [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroFechaHasta, setFiltroFechaHasta] = 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 // Estados para datos de dropdowns
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]); const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]); const [plantas, setPlantas] = useState<PlantaDto[]>([]);
@@ -62,13 +75,10 @@ const GestionarStockBobinasPage: React.FC = () => {
const [ingresoModalOpen, setIngresoModalOpen] = useState(false); const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false);
const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = 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ú // Menú de acciones
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);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedBobinaForRowMenu, setSelectedBobinaForRowMenu] = useState<StockBobinaDto | null>(null); const [selectedBobinaForRowMenu, setSelectedBobinaForRowMenu] = useState<StockBobinaDto | null>(null);
@@ -79,12 +89,9 @@ const GestionarStockBobinasPage: React.FC = () => {
const puedeModificarDatos = isSuperAdmin || tienePermiso("IB004"); const puedeModificarDatos = isSuperAdmin || tienePermiso("IB004");
const puedeEliminar = isSuperAdmin || tienePermiso("IB005"); const puedeEliminar = isSuperAdmin || tienePermiso("IB005");
const lastOpenedMenuButtonRef = useRef<HTMLButtonElement | null>(null);
const fetchFiltersDropdownData = useCallback(async () => { const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true); setLoadingFiltersDropdown(true);
try { try {
// Asumiendo que estos servicios existen y devuelven los DTOs correctos
const [tiposData, plantasData, estadosData] = await Promise.all([ const [tiposData, plantasData, estadosData] = await Promise.all([
tipoBobinaService.getAllTiposBobina(), tipoBobinaService.getAllTiposBobina(),
plantaService.getAllPlantas(), plantaService.getAllPlantas(),
@@ -121,13 +128,18 @@ const GestionarStockBobinasPage: React.FC = () => {
idPlanta: filtroPlanta ? Number(filtroPlanta) : null, idPlanta: filtroPlanta ? Number(filtroPlanta) : null,
idEstadoBobina: filtroEstadoBobina ? Number(filtroEstadoBobina) : null, idEstadoBobina: filtroEstadoBobina ? Number(filtroEstadoBobina) : null,
remitoFilter: filtroRemito || null, remitoFilter: filtroRemito || null,
// Fechas Remito
fechaDesde: filtroFechaHabilitado ? filtroFechaDesde : null, fechaDesde: filtroFechaHabilitado ? filtroFechaDesde : null,
fechaHasta: filtroFechaHabilitado ? filtroFechaHasta : 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); const data = await stockBobinaService.getAllStockBobinas(params);
setStock(data); setStock(data);
if (data.length === 0) { 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) { } catch (err) {
console.error(err); console.error(err);
@@ -135,10 +147,14 @@ const GestionarStockBobinasPage: React.FC = () => {
} finally { } finally {
setLoading(false); 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 = () => { const handleBuscarClick = () => {
setPage(0); // Resetear la paginación al buscar
cargarStock(); cargarStock();
}; };
@@ -148,14 +164,19 @@ const GestionarStockBobinasPage: React.FC = () => {
setFiltroPlanta(''); setFiltroPlanta('');
setFiltroEstadoBobina(''); setFiltroEstadoBobina('');
setFiltroRemito(''); setFiltroRemito('');
setFiltroFechaHabilitado(false); setFiltroFechaHabilitado(false);
setFiltroFechaDesde(new Date().toISOString().split('T')[0]); setFiltroFechaDesde(new Date().toISOString().split('T')[0]);
setFiltroFechaHasta(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); setError(null);
}; };
const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); };
const handleCloseIngresoModal = () => setIngresoModalOpen(false); const handleCloseIngresoModal = () => setIngresoModalOpen(false);
const handleSubmitIngresoModal = async (data: CreateStockBobinaDto) => { const handleSubmitIngresoModal = async (data: CreateStockBobinaDto) => {
setApiErrorMessage(null); 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; } 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) => { const handleLoteModalClose = (refrescar: boolean) => {
if (!bobina) return; setLoteModalOpen(false);
setSelectedBobina(bobina); if (refrescar) {
setApiErrorMessage(null); cargarStock();
setEditModalOpen(true);
};
const handleCloseEditModal = () => {
setEditModalOpen(false);
setSelectedBobina(null);
if (lastOpenedMenuButtonRef.current) {
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
} }
}; };
const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => { const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => {
setApiErrorMessage(null); setApiErrorMessage(null);
try { await stockBobinaService.updateDatosBobinaDisponible(idBobina, data); cargarStock(); } 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; } 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) => { const handleSubmitCambioEstadoModal = async (idBobina: number, data: CambiarEstadoBobinaDto) => {
setApiErrorMessage(null); setApiErrorMessage(null);
try { await stockBobinaService.cambiarEstadoBobina(idBobina, data); cargarStock(); } 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; } 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) => { const handleDeleteBobina = () => {
if (!bobina) return; if (!selectedBobinaForRowMenu) return;
if (bobina.idEstadoBobina !== ID_ESTADO_DISPONIBLE && bobina.idEstadoBobina !== ID_ESTADO_DANADA) {
if (selectedBobinaForRowMenu.idEstadoBobina !== ID_ESTADO_DISPONIBLE && selectedBobinaForRowMenu.idEstadoBobina !== ID_ESTADO_DANADA) {
alert("Solo se pueden eliminar bobinas en estado 'Disponible' o 'Dañada'."); alert("Solo se pueden eliminar bobinas en estado 'Disponible' o 'Dañada'.");
handleMenuClose(); handleMenuClose();
return; 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); setApiErrorMessage(null);
try { await stockBobinaService.deleteIngresoBobina(bobina.idBobina); cargarStock(); } stockBobinaService.deleteIngresoBobina(selectedBobinaForRowMenu.idBobina)
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); } .then(() => cargarStock())
.catch((err: any) => {
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setApiErrorMessage(msg);
});
} }
handleMenuClose(); handleMenuClose();
}; };
const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>, bobina: StockBobinaDto) => { const handleSubmitFechaRemitoModal = async (data: UpdateFechaRemitoLoteDto) => {
setAnchorEl(event.currentTarget); setApiErrorMessage(null);
setSelectedBobinaForRowMenu(bobina); try {
lastOpenedMenuButtonRef.current = event.currentTarget; await stockBobinaService.actualizarFechaRemitoLote(data);
}; cargarStock();
const handleMenuClose = () => { } catch (err: any) {
setAnchorEl(null); const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al actualizar la fecha del remito.';
setSelectedBobinaForRowMenu(null); setApiErrorMessage(msg);
if (lastOpenedMenuButtonRef.current) { throw err;
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
} }
}; };
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); // --- Handlers Menú Acciones ---
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>, bobina: StockBobinaDto) => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); 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 = { const handleMenuClose = () => {
year: 'numeric', setAnchorEl(null);
month: '2-digit',
day: '2-digit',
timeZone: 'UTC'
};
return new Intl.DateTimeFormat('es-AR', options).format(date);
}; };
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>; if (!puedeVer) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return ( return (
<Box sx={{ p: 1 }}> <Box sx={{ p: 2 }}>
<Typography variant="h5" gutterBottom>Stock de Bobinas</Typography> <Typography variant="h5" gutterBottom>Stock de Bobinas</Typography>
{/* Panel de Filtros */}
<Paper sx={{ p: 2, mb: 2 }}> <Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros</Typography> <Typography variant="h6" gutterBottom>Filtros</Typography>
{/* Fila 1: Filtros generales */}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}> <FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
<InputLabel>Tipo Bobina</InputLabel> <InputLabel>Tipo Bobina</InputLabel>
@@ -274,128 +369,123 @@ const GestionarStockBobinasPage: React.FC = () => {
</FormControl> </FormControl>
<TextField label="Remito" size="small" value={filtroRemito} onChange={(e) => setFiltroRemito(e.target.value)} sx={{ minWidth: 150, flexGrow: 1 }} /> <TextField label="Remito" size="small" value={filtroRemito} onChange={(e) => setFiltroRemito(e.target.value)} sx={{ minWidth: 150, flexGrow: 1 }} />
</Box> </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> </Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="warning" sx={{ my: 2 }}>{error}</Alert>} {error && !loading && <Alert severity="warning" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>} {apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && ( {/* Tabla DataGrid */}
<TableContainer component={Paper}> <Paper sx={{ width: '100%', height: 600 }}>
<Table size="small"> <DataGrid
<TableHead><TableRow> rows={stock}
<TableCell>Nro. Bobina</TableCell><TableCell>Tipo</TableCell><TableCell>Peso (Kg)</TableCell> columns={columns}
<TableCell>Planta</TableCell><TableCell>Estado</TableCell><TableCell>Remito</TableCell> getRowId={(row) => row.idBobina} // Importante: especificar el ID único
<TableCell>F. Remito</TableCell><TableCell>F. Estado</TableCell> loading={loading}
<TableCell>Publicación</TableCell><TableCell>Sección</TableCell> localeText={esES.components.MuiDataGrid.defaultProps.localeText}
<TableCell>Obs.</TableCell> density="compact"
{(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} disableRowSelectionOnClick
</TableRow></TableHead> initialState={{
<TableBody> pagination: { paginationModel: { pageSize: 25 } },
{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> pageSizeOptions={[25, 50, 100]}
) : ( sx={{ border: 0 }}
displayData.map((b) => ( />
<TableRow key={b.idBobina} hover> </Paper>
<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>
)}
{/* Menú Contextual de Fila */}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> <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 && ( {selectedBobinaForRowMenu && puedeModificarDatos && selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && (
<MenuItem onClick={() => { handleOpenEditModal(selectedBobinaForRowMenu); handleMenuClose(); }}> <MenuItem onClick={handleOpenEditModal}>
<EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos <EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos Bobina
</MenuItem> </MenuItem>
)} )}
{selectedBobinaForRowMenu && puedeCambiarEstado && ( {selectedBobinaForRowMenu && puedeCambiarEstado && (
<MenuItem onClick={() => { handleOpenCambioEstadoModal(selectedBobinaForRowMenu); handleMenuClose(); }}> <MenuItem onClick={handleOpenCambioEstadoModal}>
<SwapHorizIcon fontSize="small" sx={{ mr: 1 }} /> Cambiar Estado <SwapHorizIcon fontSize="small" sx={{ mr: 1 }} /> Cambiar Estado
</MenuItem> </MenuItem>
)} )}
{selectedBobinaForRowMenu && puedeEliminar && {selectedBobinaForRowMenu && puedeEliminar &&
(selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && ( (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 <DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar Ingreso
</MenuItem> </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> </Menu>
{/* Modales sin cambios */} {/* Modales */}
<StockBobinaIngresoFormModal <StockBobinaIngresoFormModal
open={ingresoModalOpen} onClose={handleCloseIngresoModal} onSubmit={handleSubmitIngresoModal} open={ingresoModalOpen} onClose={handleCloseIngresoModal} onSubmit={handleSubmitIngresoModal}
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)} errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
/> />
{editModalOpen && selectedBobina && <StockBobinaEditFormModal
<StockBobinaEditFormModal open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal} initialData={selectedBobinaForRowMenu} errorMessage={apiErrorMessage}
initialData={selectedBobina} errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
clearErrorMessage={() => setApiErrorMessage(null)} />
/> <StockBobinaCambioEstadoModal
} open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
{cambioEstadoModalOpen && selectedBobina && bobinaActual={selectedBobinaForRowMenu} errorMessage={apiErrorMessage}
<StockBobinaCambioEstadoModal clearErrorMessage={() => setApiErrorMessage(null)}
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal} />
bobinaActual={selectedBobina} errorMessage={apiErrorMessage} <StockBobinaLoteFormModal
clearErrorMessage={() => setApiErrorMessage(null)} open={loteModalOpen}
/> onClose={handleLoteModalClose}
} />
<StockBobinaFechaRemitoModal
open={fechaRemitoModalOpen}
onClose={handleCloseFechaRemitoModal}
onSubmit={handleSubmitFechaRemitoModal}
bobinaContexto={selectedBobinaForRowMenu}
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
</Box> </Box>
); );
}; };

View File

@@ -69,8 +69,8 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
setReportData(null); setReportData(null);
setDetalleDiarioCalculado([]); setDetalleDiarioCalculado([]);
setPromediosPorDiaCalculado([]); setPromediosPorDiaCalculado([]);
setTotalesDetalle({ llevados:0, devueltos:0, ventaNeta:0, promedioGeneralVentaNeta: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}); setTotalesPromedios({ cantDias: 0, promLlevados: 0, promDevueltos: 0, promVentas: 0, porcentajeDevolucionGeneral: 0 });
const pubService = (await import('../../services/Distribucion/publicacionService')).default; const pubService = (await import('../../services/Distribucion/publicacionService')).default;
@@ -89,7 +89,7 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
const llevados = item.llevados || 0; const llevados = item.llevados || 0;
const devueltos = item.devueltos || 0; const devueltos = item.devueltos || 0;
const ventaNeta = llevados - devueltos; const ventaNeta = llevados - devueltos;
if (llevados > 0) { if (llevados > 0) {
diasConActividadDetalle++; diasConActividadDetalle++;
acumuladoVentaNeta += ventaNeta; acumuladoVentaNeta += ventaNeta;
@@ -101,7 +101,7 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
...item, ...item,
id: `simple-can-${index}`, // o simple-dist-${index} id: `simple-can-${index}`, // o simple-dist-${index}
ventaNeta: ventaNeta, ventaNeta: ventaNeta,
promedio: promedioActual, promedio: promedioActual,
porcentajeDevolucion: llevados > 0 ? (devueltos / llevados) * 100 : 0, // Esto es % Devolución real porcentajeDevolucion: llevados > 0 ? (devueltos / llevados) * 100 : 0, // Esto es % Devolución real
}; };
}); });
@@ -110,45 +110,44 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
const totalLlevadosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.llevados || 0), 0); const totalLlevadosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.llevados || 0), 0);
const totalDevueltosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.devueltos || 0), 0); const totalDevueltosDetalle = detalleCalculadoLocal.reduce((sum, item) => sum + (item.devueltos || 0), 0);
const totalVentaNetaDetalle = totalLlevadosDetalle - totalDevueltosDetalle; const totalVentaNetaDetalle = totalLlevadosDetalle - totalDevueltosDetalle;
setTotalesDetalle({ setTotalesDetalle({
llevados: totalLlevadosDetalle, llevados: totalLlevadosDetalle,
devueltos: totalDevueltosDetalle, devueltos: totalDevueltosDetalle,
ventaNeta: totalVentaNetaDetalle, ventaNeta: totalVentaNetaDetalle,
promedioGeneralVentaNeta: ultimoPromedioDetalle, promedioGeneralVentaNeta: ultimoPromedioDetalle,
porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0 porcentajeDevolucionGeneral: totalLlevadosDetalle > 0 ? (totalDevueltosDetalle / totalLlevadosDetalle) * 100 : 0
}); });
// --- Cálculos para promedios y sus totales --- // --- Cálculos para promedios y sus totales ---
const promediosCalculadoLocal = data.promediosPorDia.map((item, index) => { const promediosCalculadoLocal = data.promediosPorDia.map((item, index) => {
const promLlevados = item.promedio_Llevados || 0; const promLlevados = item.promedio_Llevados || 0;
const promDevueltos = item.promedio_Devueltos || 0;
const promVentas = item.promedio_Ventas || 0; const promVentas = item.promedio_Ventas || 0;
return { return {
...item, ...item,
id: `prom-can-${index}`, // o prom-dist-${index} id: `prom-can-${index}`,
// LA COLUMNA EN EL PDF SE LLAMA "% Devolución" PERO PARECE SER "% VENTA"
porcentajeColumnaPDF: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0, porcentajeColumnaPDF: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0,
porcentajeDevolucion: promLlevados > 0 ? (promVentas / promLlevados) * 100 : 0, porcentajeDevolucion: promLlevados > 0 ? (promDevueltos / promLlevados) * 100 : 0,
}; };
}); });
setPromediosPorDiaCalculado(promediosCalculadoLocal); setPromediosPorDiaCalculado(promediosCalculadoLocal);
const totalDiasProm = promediosCalculadoLocal.reduce((sum, item) => sum + (item.cant || 0), 0); 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 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 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({ setTotalesPromedios({
cantDias: totalDiasProm, cantDias: totalDiasProm,
promLlevados: totalDiasProm > 0 ? totalPonderadoLlevados / totalDiasProm : 0, promLlevados: promGeneralLlevados,
promDevueltos: totalDiasProm > 0 ? promediosCalculadoLocal.reduce((sum, item) => sum + (item.promedio_Devueltos || 0), 0) / promediosCalculadoLocal.length :0, // Promedio simple para mostrar promDevueltos: promGeneralDevueltos,
promVentas: totalDiasProm > 0 ? totalPonderadoVentas / totalDiasProm : 0, promVentas: totalDiasProm > 0 ? totalPonderadoVentas / totalDiasProm : 0,
// Para la fila "General" de promedios, el PDF usa (Total Prom. Ventas / Total Prom. Llevados) * 100 porcentajeDevolucionGeneral: promGeneralLlevados > 0 ? (promGeneralDevueltos / promGeneralLlevados) * 100 : 0,
// Usaremos los promedios generales calculados aquí
porcentajeDevolucionGeneral: (totalDiasProm > 0 && (totalPonderadoLlevados / totalDiasProm) > 0)
? ((totalPonderadoVentas / totalDiasProm) / (totalPonderadoLlevados / totalDiasProm)) * 100
: 0,
}); });
setReportData({ detalleSimple: detalleCalculadoLocal, promediosPorDia: promediosCalculadoLocal }); setReportData({ detalleSimple: detalleCalculadoLocal, promediosPorDia: promediosCalculadoLocal });
@@ -280,16 +279,16 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
</Box> </Box>
{/* Contenedor para tus totales */} {/* Contenedor para tus totales */}
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', whiteSpace: 'nowrap', overflowX: 'auto' }}> <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: 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[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[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[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[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> <Typography variant="subtitle2" sx={{ minWidth: columnsDetalle[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesDetalle.porcentajeDevolucionGeneral.toLocaleString('es-AR', { maximumFractionDigits: 2 })}%</Typography>
</Box> </Box>
</GridFooterContainer> </GridFooterContainer>
); );
// --- Custom Footer para Promedios por Día --- // --- Custom Footer para Promedios por Día ---
const CustomFooterPromedios = () => ( const CustomFooterPromedios = () => (
<GridFooterContainer sx={{ justifyContent: 'space-between', alignItems: 'center', width: '100%' }}> <GridFooterContainer sx={{ justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
@@ -297,11 +296,11 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
<GridFooter sx={{ borderTop: 'none' }} /> <GridFooter sx={{ borderTop: 'none' }} />
</Box> </Box>
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto' }}> <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={{ 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={{ 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[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[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[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> <Typography variant="subtitle2" sx={{ minWidth: columnsPromedios[5].minWidth || 'auto', textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.porcentajeDevolucionGeneral.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</Typography>
</Box> </Box>
</GridFooterContainer> </GridFooterContainer>
@@ -372,10 +371,10 @@ const ReporteListadoDistribucionCanillasPage: React.FC = () => {
density="compact" density="compact"
slots={{ footer: CustomFooterPromedios }} slots={{ footer: CustomFooterPromedios }}
hideFooterSelectedRowCount hideFooterSelectedRowCount
sx={{ sx={{
'& .MuiTablePagination-root': { // Oculta el paginador por defecto '& .MuiTablePagination-root': { // Oculta el paginador por defecto
display: 'none', display: 'none',
}, },
}} }}
/> />
</Paper> </Paper>

View File

@@ -121,20 +121,24 @@ const ReporteListadoDistribucionPage: React.FC = () => {
porcentajeDevolucion: item.promedio_Llevados > 0 ? (item.promedio_Devueltos / item.promedio_Llevados) * 100 : 0, 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 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 countPromedios = promediosConCalculos.length;
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);
setTotalesPromedios({ // LÓGICA DE PROMEDIO DE PROMEDIOS
cantDias: totalDiasPromedios, if (countPromedios > 0) {
promLlevados: totalDiasPromedios > 0 ? totalPonderadoLlevados / totalDiasPromedios : 0, const sumPromLlevados = promediosConCalculos.reduce((sum, item) => sum + (item.promedio_Llevados || 0), 0);
promDevueltos: totalDiasPromedios > 0 ? totalPonderadoDevueltos / totalDiasPromedios : 0, const sumPromDevueltos = promediosConCalculos.reduce((sum, item) => sum + (item.promedio_Devueltos || 0), 0);
promVentas: totalDiasPromedios > 0 ? totalPonderadoVentas / totalDiasPromedios : 0, const sumPromVentas = promediosConCalculos.reduce((sum, item) => sum + (item.promedio_Ventas || 0), 0);
porcentajeDevolucionGeneral: totalPonderadoLlevados > 0 ? (totalPonderadoDevueltos / totalPonderadoLlevados) * 100 : 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 }); setReportData({ detalleSimple: detalleConCalculos, promediosPorDia: promediosConCalculos });

View File

@@ -5,8 +5,10 @@ import type { UpdateDistribuidorDto } from '../../models/dtos/Distribucion/Updat
import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto'; import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto';
import type { DistribuidorLookupDto } from '../../models/dtos/Distribucion/DistribuidorLookupDto'; import type { DistribuidorLookupDto } from '../../models/dtos/Distribucion/DistribuidorLookupDto';
const getAllDistribuidores = async (nombreFilter?: string, nroDocFilter?: string): Promise<DistribuidorDto[]> => { const getAllDistribuidores = async (nombreFilter?: string, nroDocFilter?: string, soloActivos: boolean = true): Promise<DistribuidorDto[]> => {
const params: Record<string, string> = {}; const params: Record<string, string | boolean> = {
soloActivos: soloActivos
};
if (nombreFilter) params.nombre = nombreFilter; if (nombreFilter) params.nombre = nombreFilter;
if (nroDocFilter) params.nroDoc = nroDocFilter; if (nroDocFilter) params.nroDoc = nroDocFilter;
@@ -37,11 +39,15 @@ const deleteDistribuidor = async (id: number): Promise<void> => {
await apiClient.delete(`/distribuidores/${id}`); await apiClient.delete(`/distribuidores/${id}`);
}; };
const getAllDistribuidoresDropdown = async (): Promise<DistribuidorDropdownDto[]> => { const getAllDistribuidoresDropdown = async (soloActivos: boolean = true): Promise<DistribuidorDropdownDto[]> => {
const response = await apiClient.get<DistribuidorDropdownDto[]>('/distribuidores/dropdown'); const response = await apiClient.get<DistribuidorDropdownDto[]>('/distribuidores/dropdown', { params: { soloActivos } });
return response.data; 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 = { const distribuidorService = {
getAllDistribuidores, getAllDistribuidores,
getDistribuidorById, getDistribuidorById,
@@ -50,6 +56,7 @@ const distribuidorService = {
deleteDistribuidor, deleteDistribuidor,
getAllDistribuidoresDropdown, getAllDistribuidoresDropdown,
getDistribuidorLookupById, getDistribuidorLookupById,
toggleBajaDistribuidor,
}; };
export default distribuidorService; export default distribuidorService;

View File

@@ -3,6 +3,8 @@ import type { StockBobinaDto } from '../../models/dtos/Impresion/StockBobinaDto'
import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto'; import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateStockBobinaDto';
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto'; import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto'; 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 { interface GetAllStockBobinasParams {
idTipoBobina?: number | null; idTipoBobina?: number | null;
@@ -12,6 +14,8 @@ interface GetAllStockBobinasParams {
remitoFilter?: string | null; remitoFilter?: string | null;
fechaDesde?: string | null; // "yyyy-MM-dd" fechaDesde?: string | null; // "yyyy-MM-dd"
fechaHasta?: 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[]> => { 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.remitoFilter) params.remito = filters.remitoFilter; // El backend espera remito
if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde; if (filters.fechaDesde) params.fechaDesde = filters.fechaDesde;
if (filters.fechaHasta) params.fechaHasta = filters.fechaHasta; 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 }); const response = await apiClient.get<StockBobinaDto[]>('/stockbobinas', { params });
return response.data; return response.data;
@@ -50,6 +56,23 @@ const deleteIngresoBobina = async (idBobina: number): Promise<void> => {
await apiClient.delete(`/stockbobinas/${idBobina}`); 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 = { const stockBobinaService = {
getAllStockBobinas, getAllStockBobinas,
getStockBobinaById, getStockBobinaById,
@@ -57,6 +80,9 @@ const stockBobinaService = {
updateDatosBobinaDisponible, updateDatosBobinaDisponible,
cambiarEstadoBobina, cambiarEstadoBobina,
deleteIngresoBobina, deleteIngresoBobina,
verificarRemitoExistente,
ingresarLoteBobinas,
actualizarFechaRemitoLote,
}; };
export default stockBobinaService; export default stockBobinaService;

View File

@@ -1,8 +1,8 @@
# Gestion Integral Web # 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 ## Módulos y Funcionalidades Principales