30 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
74f07df960 Fix: Sin Padding Top/Bottom en Ticket Canilla
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 1m56s
2025-11-10 10:33:45 -03:00
6ceb1477ae Fix Tamaño Impresión - Ticket Canilla
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 2m41s
2025-11-10 10:14:54 -03:00
c049c1e544 Try Limitar Log de Contenedores
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 2m20s
2025-11-05 15:16:58 -03:00
8c7278ceae Fix: Captura y Muestra del Error Por Recibo Duplicado
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 2m2s
2025-11-05 13:52:14 -03:00
e8215f8586 Fix: Zona Horaria Eliminada
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 2m29s
- Se elimina la zona horaria y se refactoriza el formateo de fecha.
2025-11-05 10:51:56 -03:00
bf7d7c22ef Fix MultipleActiveResultSets a True
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 1m45s
- Se Habilita Multiple Active Result Sets (MARS) en SQL Server para permitir a la aplicación tener más de una solicitud pendiente en una única conexión.
2025-11-04 14:13:55 -03:00
2c584e9383 feat(reportes): Permite consulta consolidada en Detalle de Distribución
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 2m34s
Implementa la funcionalidad para generar el reporte "Detalle de Distribución de Canillas" de forma consolidada para todas las empresas, separando entre Canillitas y Accionistas. Adicionalmente, se realizan correcciones y mejoras visuales en otros reportes.

### Cambios Principales

-   **Frontend (`ReporteDetalleDistribucionCanillasPage`):**
    -   Se añade la opción "TODAS" al selector de Empresas.
    -   Al seleccionar "TODAS", se muestra un nuevo control para elegir entre "Canillitas" o "Accionistas".
    -   La vista del reporte se simplifica en el modo "TODAS", mostrando solo la tabla correspondiente y ocultando el resumen por tipo de vendedor.

-   **Backend (`ReportesService`, `ReportesRepository`):**
    -   Se modifica el servicio para recibir el parámetro `esAccionista`.
    -   Se implementa una nueva lógica que, si `idEmpresa` es 0, llama a los nuevos procedimientos almacenados que consultan todas las empresas.
    -   Se ajusta el `ReportesController` para aceptar y pasar el nuevo parámetro.

### Correcciones

-   **Base de Datos:**
    -   Se añade la función SQL `FN_ObtenerPrecioVigente` para los nuevos Stored Procedures.
    -   Se crean los Stored Procedures `SP_DistCanillasEntradaSalidaPubli_AllEmpresas` y `SP_DistCanillasAccEntradaSalidaPubli_AllEmpresas`.
-   **Backend (`ReportesController`):**
    -   Se corrigen errores de compilación (`CS7036`, `CS8130`) añadiendo el parámetro `esAccionista` faltante en las llamadas al servicio `ObtenerReporteDistribucionCanillasAsync`.

### Mejoras Visuales

-   **PDF (`ControlDevolucionesDocument.cs`):**
    -   Se ajustan los espaciados verticales (`Padding` y `Spacing`) en la plantilla QuestPDF del reporte "Control de Devoluciones" para lograr un diseño más compacto.
2025-11-04 11:51:43 -03:00
e123dae182 Fix Permisos reportes secretaría y Dropdown de reporte
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 3m27s
2025-10-31 13:00:19 -03:00
c27dc2a0ba Fix deploy
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 6m15s
2025-09-10 12:30:07 -03:00
24b1c07342 Test Up
Some checks failed
Optimized Build and Deploy / remote-build-and-deploy (push) Failing after 11s
2025-09-10 11:29:25 -03:00
cb64bbc1f5 Actualizar README.md
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 3m36s
2025-08-14 13:15:23 +00:00
057310ca47 Fix(Suscripciones): Insert en db arreglado y muestra en UI tipo factura
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 3m25s
- Se arregla error al insertar en la db el registro de factura "Alta"
- Se arregla UI por falla en la visualización del tipo de factura en la tabla de gestión de facturas.
2025-08-13 15:53:34 -03:00
e95c851e5b Feat(suscripciones): Implementa facturación pro-rata para altas y excluye del débito automático
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 3m45s
Se introduce una refactorización mayor del ciclo de facturación para manejar correctamente las suscripciones que inician en un período ya cerrado. Esto soluciona el problema de cobrar un mes completo a un nuevo suscriptor, mejorando la transparencia y la experiencia del cliente.

###  Nuevas Características y Lógica de Negocio

- **Facturación Pro-rata Automática (Factura de Alta):**
    - Al crear una nueva suscripción cuya fecha de inicio corresponde a un período de facturación ya cerrado, el sistema ahora calcula automáticamente el costo proporcional por los días restantes de ese mes.
    - Se genera de forma inmediata una nueva factura de tipo "Alta" por este monto parcial, separándola del ciclo de facturación mensual regular.

- **Exclusión del Débito Automático para Facturas de Alta:**
    - Se implementa una regla de negocio clave: las facturas de tipo "Alta" son **excluidas** del proceso de generación del archivo de débito automático para el banco.
    - Esto fuerza a que el primer cobro (el proporcional) se gestione a través de un medio de pago manual (efectivo, transferencia, etc.), evitando cargos inesperados en la cuenta bancaria del cliente.
    - El débito automático comenzará a operar normalmente a partir del primer ciclo de facturación completo.

### 🔄 Cambios en el Backend

- **Base de Datos:**
    - Se ha añadido la columna `TipoFactura` (`varchar(20)`) a la tabla `susc_Facturas`.
    - Se ha implementado una `CHECK constraint` para permitir únicamente los valores 'Mensual' y 'Alta'.

- **Servicios:**
    - **`SuscripcionService`:** Ahora contiene la lógica para detectar una alta retroactiva, invocar al `FacturacionService` para el cálculo pro-rata y crear la "Factura de Alta" y su detalle correspondiente dentro de la misma transacción.
    - **`FacturacionService`:** Expone públicamente el método `CalcularImporteParaSuscripcion` y se ha actualizado `ObtenerResumenesDeCuentaPorPeriodo` para que envíe la propiedad `TipoFactura` al frontend.
    - **`DebitoAutomaticoService`:** El método `GetFacturasParaDebito` ahora filtra y excluye explícitamente las facturas donde `TipoFactura = 'Alta'`.

### 🎨 Mejoras en la Interfaz de Usuario (Frontend)

- **`ConsultaFacturasPage.tsx`:**
    - **Nueva Columna:** Se ha añadido una columna "Tipo Factura" en la tabla de detalle, que muestra un `Chip` distintivo para identificar fácilmente las facturas de "Alta".
    - **Nuevo Filtro:** Se ha agregado un nuevo menú desplegable para filtrar la vista por "Tipo de Factura" (`Todas`, `Mensual`, `Alta`), permitiendo a los administradores auditar rápidamente los nuevos ingresos.
2025-08-13 14:55:24 -03:00
038faefd35 Fix: Formato de Archivo de Débito Modificado
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 4m23s
2025-08-12 12:49:56 -03:00
da50c052f1 Fix: Configuración SMTP
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 6m46s
- No se toma la configuración SMTP del .env por falla de lecturas.
- Se incluyen las configuraciones en appsettings.json
2025-08-12 11:18:30 -03:00
5781713b13 Fix: Menú Reportes
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 7m12s
- Fix del menú de reportes que impedía el recorrido del mismo.
- Se quita la apertura predeterminada de una opción del menú de Reportes.
2025-08-12 10:33:36 -03:00
9f8d577265 Limpieza de Comantarios
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 7m59s
2025-08-11 15:44:16 -03:00
b594a48fde Feat: Se modifican visual de menú reportes
- Se limita la visual del menú de reportes a los usuarios según los permisos de acceso.
- Se soluciona bug en mensaje al ingresar usuario y/o clave inválidos.
2025-08-11 15:42:23 -03:00
2e7d1e36be Feat(suscripciones): Implementa manejo de pagos parciales en facturas
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 8m3s
Se introduce una refactorización completa del sistema de registro de pagos para manejar correctamente los abonos parciales, asegurando que el estado de la factura y el saldo pendiente se reflejen con precisión tanto en el backend como en la interfaz de usuario.

### 🐛 Problema Solucionado

- Anteriormente, el sistema no reconocía los pagos parciales. Una factura permanecía en estado "Pendiente" hasta que el monto total era cubierto, y la interfaz de usuario siempre mostraba el 100% del saldo como pendiente, lo cual era incorrecto y confuso.

###  Nuevas Características y Mejoras

- **Nuevo Estado de Factura "Pagada Parcialmente":**
    - Se introduce un nuevo estado para las facturas que han recibido uno o más pagos pero cuyo saldo aún no es cero.
    - El `PagoService` ahora actualiza el estado de la factura a "Pagada Parcialmente" cuando recibe un abono que no cubre el total.

- **Mejoras en la Interfaz de Usuario (`ConsultaFacturasPage`):**
    - **Nuevas Columnas:** Se han añadido las columnas "Pagado" y "Saldo" a la tabla de detalle de facturas, mostrando explícitamente el monto abonado y el restante.
    - **Visualización de Estado:** El `Chip` de estado ahora muestra "Pagada Parcialmente" con un color distintivo (azul/primary) para una rápida identificación visual.
    - **Cálculo de Saldo Correcto:** El saldo pendiente total por suscriptor y el saldo para el modal de pago manual ahora se calculan correctamente, restando el `totalPagado` del `importeFinal`.

### 🔄 Cambios en el Backend

- **`PagoService`:** Se actualizó la lógica para establecer el estado de la factura (`Pendiente`, `Pagada Parcialmente`, `Pagada`) basado en el `nuevoTotalPagado` después de registrar un pago.
- **`FacturacionService`:** El método `ObtenerResumenesDeCuentaPorPeriodo` ahora calcula correctamente el `SaldoPendienteTotal` y pasa la propiedad `TotalPagado` al DTO del frontend.
- **DTOs:** Se actualizó `FacturaConsolidadaDto` para incluir la propiedad `TotalPagado`.
2025-08-11 15:15:08 -03:00
dd2277fce2 Fix: Mail Host
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 9m57s
2025-08-11 14:35:02 -03:00
9412556fa8 Feat: Se añade seccion de permisos para Suscripciones
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 7m51s
- Se añade la lista de asignación de permisos de Suscripciones a la UI
- Se añade el permiso de acceso a los reportes de suscripciones
2025-08-11 12:36:51 -03:00
87 changed files with 2789 additions and 1382 deletions

View File

@@ -26,12 +26,6 @@ jobs:
set -e set -e
echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---" echo "--- INICIO DEL DESPLIEGUE OPTIMIZADO ---"
# --- Asegurar que el Stack de la Base de Datos esté corriendo ---
echo "Asegurando que el stack de la base de datos esté activo..."
cd /opt/shared-services/database
# El comando 'up -d' es idempotente. Si ya está corriendo, no hace nada.
docker compose up -d
# 1. Preparar entorno # 1. Preparar entorno
TEMP_DIR=$(mktemp -d) TEMP_DIR=$(mktemp -d)
REPO_OWNER="dmolinari" REPO_OWNER="dmolinari"

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

@@ -1,12 +0,0 @@
# ================================================
# VARIABLES DE ENTORNO PARA LA CONFIGURACIÓN DE CORREO
# ================================================
# El separador de doble guion bajo (__) se usa para mapear la jerarquía del JSON.
# MailSettings:SmtpHost se convierte en MailSettings__SmtpHost
MailSettings__SmtpHost="mail.eldia.com"
MailSettings__SmtpPort=587
MailSettings__SenderName="Club - Diario El Día"
MailSettings__SenderEmail="alertas@eldia.com"
MailSettings__SmtpUser="alertas@eldia.com"
MailSettings__SmtpPass="@Alertas713550@"

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

@@ -41,9 +41,11 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
void ComposeContent(IContainer container) void ComposeContent(IContainer container)
{ {
container.PaddingTop(1, Unit.Centimetre).Column(column => // << CAMBIO: Reducido el padding superior de 1cm a 5mm >>
container.PaddingTop(5, Unit.Millimetre).Column(column =>
{ {
column.Spacing(15); // << CAMBIO: Reducido el espaciado principal entre elementos de 15 a 10 puntos >>
column.Spacing(10);
column.Item().Row(row => column.Item().Row(row =>
{ {
@@ -59,23 +61,24 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
}); });
}); });
column.Item().PaddingTop(5).Border(1).Background(Colors.Grey.Lighten3).AlignCenter().Padding(2).Text(Model.NombreEmpresa).SemiBold(); column.Item().PaddingTop(3).Border(1).Background(Colors.Grey.Lighten3).AlignCenter().Padding(2).Text(Model.NombreEmpresa).SemiBold();
column.Item().Border(1).Padding(10).Column(innerCol => column.Item().Border(1).Padding(8).Column(innerCol => // << CAMBIO: Padding reducido de 10 a 8 >>
{ {
innerCol.Spacing(5); // << CAMBIO: Reducido el espaciado interno de 5 a 4 >>
innerCol.Spacing(4);
// Fila de "Ingresados por Remito" con borde inferior sólido.
innerCol.Item().BorderBottom(1, Unit.Point).BorderColor(Colors.Grey.Medium).Row(row => innerCol.Item().BorderBottom(1, Unit.Point).BorderColor(Colors.Grey.Medium).Row(row =>
{ {
row.RelativeItem().Text("Ingresados por Remito:").SemiBold(); row.RelativeItem().Text("Ingresados por Remito:").SemiBold();
row.RelativeItem().AlignRight().Text(Model.TotalIngresadosPorRemito.ToString("N0")); row.RelativeItem().AlignRight().Text(Model.TotalIngresadosPorRemito.ToString("N0"));
}); // <-- SOLUCIÓN: Borde sólido simple. });
foreach (var item in Model.Detalles) foreach (var item in Model.Detalles)
{ {
var totalSeccion = item.Devueltos - item.Llevados; var totalSeccion = item.Devueltos - item.Llevados;
innerCol.Item().PaddingTop(5).Row(row => // << CAMBIO: Reducido el padding superior de 5 a 3 >>
innerCol.Item().PaddingTop(3).Row(row =>
{ {
row.ConstantItem(100).Text(item.Tipo).SemiBold(); row.ConstantItem(100).Text(item.Tipo).SemiBold();
@@ -90,7 +93,8 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
r.RelativeItem().Text("Devueltos"); r.RelativeItem().Text("Devueltos");
r.RelativeItem().AlignRight().Text($"{item.Devueltos:N0}"); r.RelativeItem().AlignRight().Text($"{item.Devueltos:N0}");
}); });
sub.Item().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(2).Row(r => { // << CAMBIO: Reducido el padding superior de 2 a 1 >>
sub.Item().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(1).Row(r => {
r.RelativeItem().Text(t => t.Span("Total").SemiBold()); r.RelativeItem().Text(t => t.Span("Total").SemiBold());
r.RelativeItem().AlignRight().Text(t => t.Span(totalSeccion.ToString("N0")).SemiBold()); r.RelativeItem().AlignRight().Text(t => t.Span(totalSeccion.ToString("N0")).SemiBold());
}); });
@@ -99,7 +103,8 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
} }
}); });
column.Item().PaddingTop(10).Column(finalCol => // << CAMBIO: Reducido el padding superior de 10 a 8 >>
column.Item().PaddingTop(8).Column(finalCol =>
{ {
finalCol.Spacing(2); finalCol.Spacing(2);
@@ -112,13 +117,15 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
if (isBold) valueText.SemiBold(); if (isBold) valueText.SemiBold();
}; };
// Usamos bordes superiores para separar las líneas de total // << CAMBIO: Reducido el padding superior de 2 a 1 en las siguientes líneas >>
finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución a la Fecha", Model.TotalDevolucionALaFecha.ToString("N0"), false)); finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución a la Fecha", Model.TotalDevolucionALaFecha.ToString("N0"), false));
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución Días Anteriores", Model.TotalDevolucionDiasAnteriores.ToString("N0"), false)); finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución Días Anteriores", Model.TotalDevolucionDiasAnteriores.ToString("N0"), false));
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Total Devolución", Model.TotalDevolucionGeneral.ToString("N0"), false)); finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Total Devolución", Model.TotalDevolucionGeneral.ToString("N0"), false));
finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(5).Row(row => AddTotalRow(row, "Sin Cargo", Model.TotalSinCargo.ToString("N0"), false)); // << CAMBIO: Reducido el padding superior de 5 a 3 >>
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(2).Row(row => AddTotalRow(row, "Sobrantes", $"-{Model.TotalSobrantes.ToString("N0")}", false)); finalCol.Item().BorderTop(2f).BorderColor(Colors.Black).PaddingTop(3).Row(row => AddTotalRow(row, "Sin Cargo", Model.TotalSinCargo.ToString("N0"), false));
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(5).Row(row => AddTotalRow(row, "Diferencia", Model.DiferenciaFinal.ToString("N0"), true)); finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(1).Row(row => AddTotalRow(row, "Sobrantes", $"-{Model.TotalSobrantes.ToString("N0")}", false));
// << CAMBIO: Reducido el padding superior de 5 a 3 >>
finalCol.Item().BorderTop(1).BorderColor(Colors.Grey.Lighten2).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingTop(3).Row(row => AddTotalRow(row, "Diferencia", Model.DiferenciaFinal.ToString("N0"), true));
}); });
}); });
} }

View File

@@ -18,23 +18,32 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
} }
public DocumentMetadata GetMetadata() => DocumentMetadata.Default; public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
// CORRECCIÓN: El método GetSettings ya no es necesario para este diseño.
// La configuración por defecto es suficiente.
// public DocumentSettings GetSettings() => DocumentSettings.Default;
public void Compose(IDocumentContainer container) public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(5, Unit.Millimetre);
page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(9));
page.Content().Column(mainColumn =>
{ {
container.Page(page => mainColumn.Item()
{ .AlignCenter()
page.Size(PageSizes.A5); .Width(PageSizes.A6.Width)
page.Margin(1, Unit.Centimetre); .Height(PageSizes.A6.Height)
page.DefaultTextStyle(x => x.FontFamily("Arial").FontSize(9)); .Column(a6ContentColumn =>
{
page.Header().Element(ComposeHeader); a6ContentColumn.Item().PaddingRight(10, Unit.Millimetre).PaddingLeft(10, Unit.Millimetre).Column(content =>
page.Content().Element(ComposeContent); {
}); content.Item().Element(ComposeHeader);
} content.Item().Element(ComposeContent);
});
});
});
});
}
void ComposeHeader(IContainer container) void ComposeHeader(IContainer container)
{ {
@@ -50,13 +59,13 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
column.Item().PaddingTop(10); column.Item().PaddingTop(10);
}); });
} }
void ComposeContent(IContainer container) void ComposeContent(IContainer container)
{ {
container.Column(column => container.Column(column =>
{ {
column.Spacing(15); column.Spacing(15);
column.Item().Table(table => column.Item().Table(table =>
{ {
table.ColumnsDefinition(columns => table.ColumnsDefinition(columns =>
@@ -71,15 +80,15 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
var vendidos = item.TotalCantSalida - item.TotalCantEntrada; var vendidos = item.TotalCantSalida - item.TotalCantEntrada;
table.Cell().ColumnSpan(3).Text(item.Publicacion).SemiBold(); table.Cell().ColumnSpan(3).Text(item.Publicacion).SemiBold();
table.Cell(); table.Cell();
table.Cell().Text("Retirados"); table.Cell().Text("Retirados");
table.Cell().AlignRight().Text(item.TotalCantSalida.ToString("N0")); table.Cell().AlignRight().Text(item.TotalCantSalida.ToString("N0"));
table.Cell(); table.Cell();
table.Cell().Text("Devueltos"); table.Cell().Text("Devueltos");
table.Cell().AlignRight().Text(item.TotalCantEntrada.ToString("N0")); table.Cell().AlignRight().Text(item.TotalCantEntrada.ToString("N0"));
table.Cell(); table.Cell();
table.Cell().Text("Vendidos"); table.Cell().Text("Vendidos");
table.Cell().AlignRight().Text(vendidos.ToString("N0")); table.Cell().AlignRight().Text(vendidos.ToString("N0"));
@@ -87,7 +96,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
table.Cell(); table.Cell();
table.Cell().Text("Precio Unitario"); table.Cell().Text("Precio Unitario");
table.Cell().AlignRight().Text(item.PrecioEjemplar.ToString("C", CultureAr)); table.Cell().AlignRight().Text(item.PrecioEjemplar.ToString("C", CultureAr));
table.Cell(); table.Cell();
table.Cell().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(2).Text(t => t.Span("Importe Vendido").SemiBold()); table.Cell().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(2).Text(t => t.Span("Importe Vendido").SemiBold());
table.Cell().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(2).AlignRight().Text(t => t.Span(item.TotalRendir.ToString("C", CultureAr)).SemiBold()); table.Cell().BorderTop(1.5f).BorderColor(Colors.Black).PaddingTop(2).AlignRight().Text(t => t.Span(item.TotalRendir.ToString("C", CultureAr)).SemiBold());
@@ -99,20 +108,20 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
row.RelativeItem().Text("Total A Rendir").SemiBold().FontSize(10); row.RelativeItem().Text("Total A Rendir").SemiBold().FontSize(10);
row.RelativeItem().AlignRight().Text(text => text.Span(Model.TotalARendir.ToString("C", CultureAr)).SemiBold().FontSize(10)); row.RelativeItem().AlignRight().Text(text => text.Span(Model.TotalARendir.ToString("C", CultureAr)).SemiBold().FontSize(10));
}); });
if (!Model.EsAccionista && Model.Ganancias.Any()) if (!Model.EsAccionista && Model.Ganancias.Any())
{ {
column.Item().PaddingTop(10).Element(ComposeGananciasTable); column.Item().PaddingTop(10).Element(ComposeGananciasTable);
} }
}); });
} }
void ComposeGananciasTable(IContainer container) void ComposeGananciasTable(IContainer container)
{ {
container.Column(column => container.Column(column =>
{ {
column.Item().Text("Comisiones Acreditadas").SemiBold().FontSize(11); column.Item().Text("Comisiones Acreditadas").SemiBold().FontSize(11);
column.Item().PaddingTop(5).Table(table => column.Item().PaddingTop(5).Table(table =>
{ {
table.ColumnsDefinition(columns => table.ColumnsDefinition(columns =>
@@ -126,7 +135,7 @@ namespace GestionIntegral.Api.Controllers.Reportes.PdfTemplates
table.Cell().Border(1).BorderColor(Colors.Grey.Lighten2).Padding(3).Text(item.Publicacion); table.Cell().Border(1).BorderColor(Colors.Grey.Lighten2).Padding(3).Text(item.Publicacion);
table.Cell().Border(1).BorderColor(Colors.Grey.Lighten2).Padding(3).AlignRight().Text(item.TotalRendir.ToString("C", CultureAr)); table.Cell().Border(1).BorderColor(Colors.Grey.Lighten2).Padding(3).AlignRight().Text(item.TotalRendir.ToString("C", CultureAr));
} }
table.Cell().BorderTop(1.5f).BorderColor(Colors.Black).Padding(3).Text("Total Comisiones").SemiBold(); table.Cell().BorderTop(1.5f).BorderColor(Colors.Black).Padding(3).Text("Total Comisiones").SemiBold();
table.Cell().BorderTop(1.5f).BorderColor(Colors.Black).Padding(3).AlignRight().Text(t => t.Span(Model.TotalComisiones.ToString("C", CultureAr)).SemiBold()); table.Cell().BorderTop(1.5f).BorderColor(Colors.Black).Padding(3).AlignRight().Text(t => t.Span(Model.TotalComisiones.ToString("C", CultureAr)).SemiBold());
}); });

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

@@ -40,6 +40,7 @@ namespace GestionIntegral.Api.Controllers
private const string PermisoVerReporteListadoDistMensual = "RR009"; private const string PermisoVerReporteListadoDistMensual = "RR009";
private const string PermisoVerReporteFacturasPublicidad = "RR010"; private const string PermisoVerReporteFacturasPublicidad = "RR010";
private const string PermisoVerReporteDistSuscripciones = "RR011"; private const string PermisoVerReporteDistSuscripciones = "RR011";
private const string PermisoVerReportesSecretaria = "RR012";
public ReportesController( public ReportesController(
IReportesService reportesService, IReportesService reportesService,
@@ -526,7 +527,7 @@ namespace GestionIntegral.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetVentaMensualSecretariaElDia([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) public async Task<IActionResult> GetVentaMensualSecretariaElDia([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
{ {
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002 para todos estos if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002 para todos estos
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta); var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta);
if (error != null) return BadRequest(new { message = error }); if (error != null) return BadRequest(new { message = error });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Día'." }); if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Día'." });
@@ -540,7 +541,7 @@ namespace GestionIntegral.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetVentaMensualSecretariaElDiaPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) public async Task<IActionResult> GetVentaMensualSecretariaElDiaPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
{ {
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid();
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta); var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElDiaAsync(fechaDesde, fechaHasta);
@@ -577,7 +578,7 @@ namespace GestionIntegral.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetVentaMensualSecretariaElPlata([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) public async Task<IActionResult> GetVentaMensualSecretariaElPlata([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
{ {
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002 if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta); var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta);
if (error != null) return BadRequest(new { message = error }); if (error != null) return BadRequest(new { message = error });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Plata'." }); if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de ventas 'El Plata'." });
@@ -591,7 +592,7 @@ namespace GestionIntegral.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetVentaMensualSecretariaElPlataPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) public async Task<IActionResult> GetVentaMensualSecretariaElPlataPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
{ {
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid();
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta); var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaElPlataAsync(fechaDesde, fechaHasta);
@@ -628,7 +629,7 @@ namespace GestionIntegral.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetVentaMensualSecretariaTirDevo([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) public async Task<IActionResult> GetVentaMensualSecretariaTirDevo([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
{ {
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); // Asumiendo RR002 if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid(); // Asumiendo RR002
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta); var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta);
if (error != null) return BadRequest(new { message = error }); if (error != null) return BadRequest(new { message = error });
if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de tirada/devolución." }); if (data == null || !data.Any()) return NotFound(new { message = "No hay datos para el reporte de tirada/devolución." });
@@ -642,7 +643,7 @@ namespace GestionIntegral.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetVentaMensualSecretariaTirDevoPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta) public async Task<IActionResult> GetVentaMensualSecretariaTirDevoPdf([FromQuery] DateTime fechaDesde, [FromQuery] DateTime fechaHasta)
{ {
if (!TienePermiso(PermisoVerListadoDistribucion)) return Forbid(); if (!TienePermiso(PermisoVerReportesSecretaria)) return Forbid();
var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta); var (data, error) = await _reportesService.ObtenerVentaMensualSecretariaTirDevoAsync(fechaDesde, fechaHasta);
@@ -677,13 +678,18 @@ namespace GestionIntegral.Api.Controllers
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetReporteDistribucionCanillasData([FromQuery] DateTime fecha, [FromQuery] int idEmpresa) public async Task<IActionResult> GetReporteDistribucionCanillasData(
[FromQuery] DateTime fecha,
[FromQuery] int idEmpresa,
[FromQuery] bool? esAccionista
)
{ {
if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid(); if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid();
// Pasar el nuevo parámetro al servicio
var (canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq, var (canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq,
ctrlDevolucionesRemitos, ctrlDevolucionesParaDistCan, ctrlDevolucionesOtrosDias, error) = ctrlDevolucionesRemitos, ctrlDevolucionesParaDistCan, ctrlDevolucionesOtrosDias, error) =
await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, esAccionista);
if (error != null) return BadRequest(new { message = error }); if (error != null) return BadRequest(new { message = error });
@@ -718,14 +724,20 @@ namespace GestionIntegral.Api.Controllers
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetReporteDistribucionCanillasPdf([FromQuery] DateTime fecha, [FromQuery] int idEmpresa, [FromQuery] bool soloTotales = false) public async Task<IActionResult> GetReporteDistribucionCanillasPdf(
[FromQuery] DateTime fecha,
[FromQuery] int idEmpresa,
[FromQuery] bool? esAccionista,
[FromQuery] bool soloTotales = false
)
{ {
if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid(); if (!TienePermiso(PermisoVerComprobanteLiquidacionCanilla)) return Forbid();
// Pasar el nuevo parámetro al servicio
var ( var (
canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq, canillas, canillasAcc, canillasAll, canillasFechaLiq, canillasAccFechaLiq,
remitos, ctrlDevoluciones, _, error remitos, ctrlDevoluciones, _, error
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); ) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, esAccionista);
if (error != null) return BadRequest(new { message = error }); if (error != null) return BadRequest(new { message = error });
@@ -794,11 +806,11 @@ namespace GestionIntegral.Api.Controllers
_, // canillasAll _, // canillasAll
_, // canillasFechaLiq _, // canillasFechaLiq
_, // canillasAccFechaLiq _, // canillasAccFechaLiq
ctrlDevolucionesRemitosData, // Para SP_ObtenerCtrlDevoluciones -> DataSet "DSObtenerCtrlDevoluciones" ctrlDevolucionesRemitosData,
ctrlDevolucionesParaDistCanData, // Para SP_DistCanillasCantidadEntradaSalida -> DataSet "DSCtrlDevoluciones" ctrlDevolucionesParaDistCanData,
ctrlDevolucionesOtrosDiasData, // Para SP_DistCanillasCantidadEntradaSalidaOtrosDias -> DataSet "DSCtrlDevolucionesOtrosDias" ctrlDevolucionesOtrosDiasData,
error error
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); // Reutilizamos este método ) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, null);
if (error != null) return BadRequest(new { message = error }); if (error != null) return BadRequest(new { message = error });
@@ -832,7 +844,7 @@ namespace GestionIntegral.Api.Controllers
var ( var (
_, _, _, _, _, // Datos no utilizados _, _, _, _, _, // Datos no utilizados
remitos, detalles, otrosDias, error remitos, detalles, otrosDias, error
) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa); ) = await _reportesService.ObtenerReporteDistribucionCanillasAsync(fecha, idEmpresa, null);
if (error != null) return BadRequest(new { message = error }); if (error != null) return BadRequest(new { message = error });

View File

@@ -72,15 +72,16 @@ namespace GestionIntegral.Api.Controllers.Suscripciones
[HttpGet("{anio:int}/{mes:int}")] [HttpGet("{anio:int}/{mes:int}")]
public async Task<IActionResult> GetFacturas( public async Task<IActionResult> GetFacturas(
int anio, int mes, int anio, int mes,
[FromQuery] string? nombreSuscriptor, [FromQuery] string? nombreSuscriptor,
[FromQuery] string? estadoPago, [FromQuery] string? estadoPago,
[FromQuery] string? estadoFacturacion) [FromQuery] string? estadoFacturacion,
[FromQuery] string? tipoFactura)
{ {
if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid(); if (!TienePermiso(PermisoGestionarFacturacion)) return Forbid();
if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El período no es válido." }); if (anio < 2020 || mes < 1 || mes > 12) return BadRequest(new { message = "El período no es válido." });
var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion); var resumenes = await _facturacionService.ObtenerResumenesDeCuentaPorPeriodo(anio, mes, nombreSuscriptor, estadoPago, estadoFacturacion, tipoFactura);
return Ok(resumenes); return Ok(resumenes);
} }

View File

@@ -16,7 +16,7 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
Task<PagoDistribuidor?> CreateAsync(PagoDistribuidor nuevoPago, int idUsuario, IDbTransaction transaction); Task<PagoDistribuidor?> CreateAsync(PagoDistribuidor nuevoPago, int idUsuario, IDbTransaction transaction);
Task<bool> UpdateAsync(PagoDistribuidor pagoAActualizar, int idUsuario, IDbTransaction transaction); Task<bool> UpdateAsync(PagoDistribuidor pagoAActualizar, int idUsuario, IDbTransaction transaction);
Task<bool> DeleteAsync(int idPago, int idUsuario, IDbTransaction transaction); Task<bool> DeleteAsync(int idPago, int idUsuario, IDbTransaction transaction);
Task<bool> ExistsByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null); Task<PagoDistribuidor?> GetByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null);
Task<IEnumerable<(PagoDistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync( Task<IEnumerable<(PagoDistribuidorHistorico Historial, string NombreUsuarioModifico)>> GetHistorialAsync(
DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaDesde, DateTime? fechaHasta,
int? idUsuarioModifico, string? tipoModificacion, int? idUsuarioModifico, string? tipoModificacion,

View File

@@ -70,9 +70,10 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
} }
} }
public async Task<bool> ExistsByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null) public async Task<PagoDistribuidor?> GetByReciboAndTipoMovimientoAsync(int recibo, string tipoMovimiento, int? excludeIdPago = null)
{ {
var sqlBuilder = new StringBuilder("SELECT COUNT(1) FROM dbo.cue_PagosDistribuidor WHERE Recibo = @ReciboParam AND TipoMovimiento = @TipoMovParam"); var sqlBuilder = new StringBuilder(SelectQueryBase()); // Reutiliza la consulta base
sqlBuilder.Append(" WHERE Recibo = @ReciboParam AND TipoMovimiento = @TipoMovParam");
var parameters = new DynamicParameters(); var parameters = new DynamicParameters();
parameters.Add("ReciboParam", recibo); parameters.Add("ReciboParam", recibo);
parameters.Add("TipoMovParam", tipoMovimiento); parameters.Add("TipoMovParam", tipoMovimiento);
@@ -85,12 +86,12 @@ namespace GestionIntegral.Api.Data.Repositories.Contables
try try
{ {
using var connection = _cf.CreateConnection(); using var connection = _cf.CreateConnection();
return await connection.ExecuteScalarAsync<bool>(sqlBuilder.ToString(), parameters); return await connection.QuerySingleOrDefaultAsync<PagoDistribuidor>(sqlBuilder.ToString(), parameters);
} }
catch (Exception ex) catch (Exception ex)
{ {
_log.LogError(ex, "Error en ExistsByReciboAndTipoMovimientoAsync. Recibo: {Recibo}, Tipo: {Tipo}", recibo, tipoMovimiento); _log.LogError(ex, "Error en GetByReciboAndTipoMovimientoAsync. Recibo: {Recibo}, Tipo: {Tipo}", recibo, tipoMovimiento);
return true; // Asumir que existe en caso de error para prevenir duplicados throw; // Relanzar para que el servicio lo maneje
} }
} }

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

@@ -48,5 +48,7 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
Task<IEnumerable<FacturasParaReporteDto>> GetDatosReportePublicidadAsync(string periodo); Task<IEnumerable<FacturasParaReporteDto>> GetDatosReportePublicidadAsync(string periodo);
Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesActivasAsync(DateTime fechaDesde, DateTime fechaHasta); Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesActivasAsync(DateTime fechaDesde, DateTime fechaHasta);
Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesBajasAsync(DateTime fechaDesde, DateTime fechaHasta); Task<IEnumerable<DistribucionSuscripcionDto>> GetDistribucionSuscripcionesBajasAsync(DateTime fechaDesde, DateTime fechaHasta);
Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(DateTime fecha);
Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(DateTime fecha);
} }
} }

View File

@@ -653,5 +653,39 @@ namespace GestionIntegral.Api.Data.Repositories.Reportes
return Enumerable.Empty<DistribucionSuscripcionDto>(); return Enumerable.Empty<DistribucionSuscripcionDto>();
} }
} }
public async Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(DateTime fecha)
{
const string spName = "dbo.SP_DistCanillasEntradaSalidaPubli_AllEmpresas";
var parameters = new DynamicParameters();
parameters.Add("@fecha", fecha, DbType.DateTime);
try
{
using var connection = _dbConnectionFactory.CreateConnection();
return await connection.QueryAsync<DetalleDistribucionCanillaDto>(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error SP {SPName}", spName);
return Enumerable.Empty<DetalleDistribucionCanillaDto>();
}
}
public async Task<IEnumerable<DetalleDistribucionCanillaDto>> GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(DateTime fecha)
{
const string spName = "dbo.SP_DistCanillasAccEntradaSalidaPubli_AllEmpresas";
var parameters = new DynamicParameters();
parameters.Add("@fecha", fecha, DbType.DateTime);
try
{
using var connection = _dbConnectionFactory.CreateConnection();
return await connection.QueryAsync<DetalleDistribucionCanillaDto>(spName, parameters, commandType: CommandType.StoredProcedure, commandTimeout: 120);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error SP {SPName}", spName);
return Enumerable.Empty<DetalleDistribucionCanillaDto>();
}
}
} }
} }

View File

@@ -59,10 +59,15 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
{ {
throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas."); throw new ArgumentNullException(nameof(transaction), "La transacción o su conexión no pueden ser nulas.");
} }
const string sqlInsert = @" const string sqlInsert = @"
INSERT INTO dbo.susc_Facturas (IdSuscriptor, Periodo, FechaEmision, FechaVencimiento, ImporteBruto, DescuentoAplicado, ImporteFinal, EstadoPago, EstadoFacturacion) INSERT INTO dbo.susc_Facturas
(IdSuscriptor, Periodo, FechaEmision, FechaVencimiento, ImporteBruto,
DescuentoAplicado, ImporteFinal, EstadoPago, EstadoFacturacion, TipoFactura)
OUTPUT INSERTED.* OUTPUT INSERTED.*
VALUES (@IdSuscriptor, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto, @DescuentoAplicado, @ImporteFinal, @EstadoPago, @EstadoFacturacion);"; VALUES
(@IdSuscriptor, @Periodo, @FechaEmision, @FechaVencimiento, @ImporteBruto,
@DescuentoAplicado, @ImporteFinal, @EstadoPago, @EstadoFacturacion, @TipoFactura);";
return await transaction.Connection.QuerySingleAsync<Factura>(sqlInsert, nuevaFactura, transaction); return await transaction.Connection.QuerySingleAsync<Factura>(sqlInsert, nuevaFactura, transaction);
} }
@@ -104,7 +109,8 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
return rowsAffected == idsFacturas.Count(); return rowsAffected == idsFacturas.Count();
} }
public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion) public async Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(
string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura)
{ {
var sqlBuilder = new StringBuilder(@" var sqlBuilder = new StringBuilder(@"
WITH FacturaConEmpresa AS ( WITH FacturaConEmpresa AS (
@@ -149,6 +155,12 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
parameters.Add("EstadoFacturacion", estadoFacturacion); parameters.Add("EstadoFacturacion", estadoFacturacion);
} }
if (!string.IsNullOrWhiteSpace(tipoFactura))
{
sqlBuilder.Append(" AND f.TipoFactura = @TipoFactura");
parameters.Add("TipoFactura", tipoFactura);
}
sqlBuilder.Append(" ORDER BY s.NombreCompleto, f.IdFactura;"); sqlBuilder.Append(" ORDER BY s.NombreCompleto, f.IdFactura;");
try try

View File

@@ -15,7 +15,8 @@ namespace GestionIntegral.Api.Data.Repositories.Suscripciones
Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction); Task<bool> UpdateEstadoPagoAsync(int idFactura, string nuevoEstadoPago, IDbTransaction transaction);
Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction); Task<bool> UpdateNumeroFacturaAsync(int idFactura, string numeroFactura, IDbTransaction transaction);
Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction); Task<bool> UpdateLoteDebitoAsync(IEnumerable<int> idsFacturas, int idLoteDebito, IDbTransaction transaction);
Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion); Task<IEnumerable<(Factura Factura, string NombreSuscriptor, int IdEmpresa, decimal TotalPagado)>> GetByPeriodoEnrichedAsync(
string periodo, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura);
Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction); Task<bool> UpdateEstadoYMotivoAsync(int idFactura, string nuevoEstadoPago, string? motivoRechazo, IDbTransaction transaction);
Task<string?> GetUltimoPeriodoFacturadoAsync(); Task<string?> GetUltimoPeriodoFacturadoAsync();
Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo); Task<IEnumerable<Factura>> GetFacturasPagadasPendientesDeFacturar(string periodo);

View File

@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>4923d7ee-0944-456c-abcd-d6ce13ba8485</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" /> <PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
<PackageReference Include="Dapper" Version="2.1.66" /> <PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
<PackageReference Include="MailKit" Version="4.13.0" /> <PackageReference Include="MailKit" Version="4.13.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />

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

@@ -8,6 +8,9 @@ namespace GestionIntegral.Api.Dtos.Suscripciones
public string EstadoPago { get; set; } = string.Empty; public string EstadoPago { get; set; } = string.Empty;
public string EstadoFacturacion { get; set; } = string.Empty; public string EstadoFacturacion { get; set; } = string.Empty;
public string? NumeroFactura { get; set; } public string? NumeroFactura { get; set; }
public decimal TotalPagado { get; set; }
public string TipoFactura { get; set; } = string.Empty;
public int IdSuscriptor { get; set; }
public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>(); public List<FacturaDetalleDto> Detalles { get; set; } = new List<FacturaDetalleDto>();
} }
} }

View File

@@ -15,5 +15,6 @@ namespace GestionIntegral.Api.Models.Suscripciones
public string? NumeroFactura { get; set; } public string? NumeroFactura { get; set; }
public int? IdLoteDebito { get; set; } public int? IdLoteDebito { get; set; }
public string? MotivoRechazo { get; set; } public string? MotivoRechazo { get; set; }
public string TipoFactura { get; set; } = string.Empty;
} }
} }

View File

@@ -24,10 +24,6 @@ using GestionIntegral.Api.Models.Comunicaciones;
using GestionIntegral.Api.Services.Comunicaciones; using GestionIntegral.Api.Services.Comunicaciones;
using GestionIntegral.Api.Data.Repositories.Comunicaciones; using GestionIntegral.Api.Data.Repositories.Comunicaciones;
// Carga las variables de entorno desde el archivo .env al inicio de la aplicación.
// Debe ser la primera línea para que la configuración esté disponible para el 'builder'.
DotNetEnv.Env.Load();
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// --- Registros de Servicios --- // --- Registros de Servicios ---

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

@@ -4,6 +4,8 @@ using MailKit.Net.Smtp;
using MailKit.Security; using MailKit.Security;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MimeKit; using MimeKit;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace GestionIntegral.Api.Services.Comunicaciones namespace GestionIntegral.Api.Services.Comunicaciones
{ {
@@ -88,6 +90,30 @@ namespace GestionIntegral.Api.Services.Comunicaciones
using var smtp = new SmtpClient(); using var smtp = new SmtpClient();
try try
{ {
// Se añade una política de validación de certificado personalizada.
// Esto es necesario para entornos de desarrollo o redes internas donde
// el nombre del host al que nos conectamos (ej. una IP) no coincide
// con el nombre en el certificado SSL (ej. mail.eldia.com).
smtp.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
// Si no hay errores, el certificado es válido.
if (sslPolicyErrors == SslPolicyErrors.None)
return true;
// Si el único error es que el nombre no coincide (RemoteCertificateNameMismatch)
// Y el certificado es el que esperamos (emitido para "mail.eldia.com"),
// entonces lo aceptamos como válido.
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch) && certificate != null && certificate.Subject.Contains("CN=mail.eldia.com"))
{
_logger.LogWarning("Se aceptó un certificado SSL con 'Name Mismatch' para el host de confianza 'mail.eldia.com'.");
return true;
}
// Para cualquier otro error, rechazamos el certificado.
_logger.LogError("Error de validación de certificado SSL: {Errors}", sslPolicyErrors);
return false;
};
await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls); await smtp.ConnectAsync(_mailSettings.SmtpHost, _mailSettings.SmtpPort, SecureSocketOptions.StartTls);
await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass); await smtp.AuthenticateAsync(_mailSettings.SmtpUser, _mailSettings.SmtpPass);
await smtp.SendAsync(emailMessage); await smtp.SendAsync(emailMessage);
@@ -95,20 +121,6 @@ namespace GestionIntegral.Api.Services.Comunicaciones
log.Estado = "Enviado"; log.Estado = "Enviado";
_logger.LogInformation("Email enviado exitosamente a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject); _logger.LogInformation("Email enviado exitosamente a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);
} }
catch (SmtpCommandException scEx)
{
_logger.LogError(scEx, "Error de comando SMTP al enviar a {Destinatario}. StatusCode: {StatusCode}", destinatario, scEx.StatusCode);
log.Estado = "Fallido";
log.Error = $"Error del servidor: ({scEx.StatusCode}) {scEx.Message}";
throw;
}
catch (AuthenticationException authEx)
{
_logger.LogError(authEx, "Error de autenticación con el servidor SMTP.");
log.Estado = "Fallido";
log.Error = "Error de autenticación. Revise las credenciales de correo.";
throw;
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error general al enviar email a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject); _logger.LogError(ex, "Error general al enviar email a {Destinatario}. Asunto: {Asunto}", destinatario, emailMessage.Subject);

View File

@@ -93,8 +93,18 @@ namespace GestionIntegral.Api.Services.Contables
return (null, "Tipo de pago no válido."); return (null, "Tipo de pago no válido.");
if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null) if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null)
return (null, "Empresa no válida."); return (null, "Empresa no válida.");
if (await _pagoRepo.ExistsByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento)) var pagoExistente = await _pagoRepo.GetByReciboAndTipoMovimientoAsync(createDto.Recibo, createDto.TipoMovimiento);
return (null, $"Ya existe un pago '{createDto.TipoMovimiento}' con el número de recibo '{createDto.Recibo}'."); if (pagoExistente != null)
{
// Si encontramos un duplicado, obtenemos los detalles para el mensaje de error
var distribuidor = await _distribuidorRepo.GetByIdSimpleAsync(pagoExistente.IdDistribuidor);
var empresa = await _empresaRepo.GetByIdAsync(pagoExistente.IdEmpresa);
string mensajeError = $"El recibo N° {createDto.Recibo} ya fue registrado como '{pagoExistente.TipoMovimiento}' el {pagoExistente.Fecha:dd/MM/yyyy} " +
$"para el distribuidor '{distribuidor?.Nombre ?? "Desconocido"}' en la empresa '{empresa?.Nombre ?? "Desconocida"}'.";
return (null, mensajeError);
}
var nuevoPago = new PagoDistribuidor var nuevoPago = new PagoDistribuidor
{ {
@@ -270,30 +280,30 @@ namespace GestionIntegral.Api.Services.Contables
} }
} }
} }
public async Task<IEnumerable<PagoDistribuidorHistorialDto>> ObtenerHistorialAsync( public async Task<IEnumerable<PagoDistribuidorHistorialDto>> ObtenerHistorialAsync(
DateTime? fechaDesde, DateTime? fechaHasta, DateTime? fechaDesde, DateTime? fechaHasta,
int? idUsuarioModifico, string? tipoModificacion, int? idUsuarioModifico, string? tipoModificacion,
int? idPagoAfectado) int? idPagoAfectado)
{
var historialData = await _pagoRepo.GetHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idPagoAfectado);
return historialData.Select(h => new PagoDistribuidorHistorialDto
{ {
Id_Pago = h.Historial.Id_Pago, var historialData = await _pagoRepo.GetHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idPagoAfectado);
Id_Distribuidor = h.Historial.Id_Distribuidor,
Fecha = h.Historial.Fecha, return historialData.Select(h => new PagoDistribuidorHistorialDto
TipoMovimiento = h.Historial.TipoMovimiento, {
Recibo = h.Historial.Recibo, Id_Pago = h.Historial.Id_Pago,
Monto = h.Historial.Monto, Id_Distribuidor = h.Historial.Id_Distribuidor,
Id_TipoPago = h.Historial.Id_TipoPago, Fecha = h.Historial.Fecha,
Detalle = h.Historial.Detalle, TipoMovimiento = h.Historial.TipoMovimiento,
Id_Empresa = h.Historial.Id_Empresa, Recibo = h.Historial.Recibo,
Id_Usuario = h.Historial.Id_Usuario, Monto = h.Historial.Monto,
NombreUsuarioModifico = h.NombreUsuarioModifico, Id_TipoPago = h.Historial.Id_TipoPago,
FechaMod = h.Historial.FechaMod, Detalle = h.Historial.Detalle,
TipoMod = h.Historial.TipoMod Id_Empresa = h.Historial.Id_Empresa,
}).ToList(); Id_Usuario = h.Historial.Id_Usuario,
} NombreUsuarioModifico = h.NombreUsuarioModifico,
FechaMod = h.Historial.FechaMod,
TipoMod = h.Historial.TipoMod
}).ToList();
}
} }
} }

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

@@ -27,16 +27,16 @@ namespace GestionIntegral.Api.Services.Reportes
// Reporte Distribucion Canillas (MC005) - Este es un reporte más complejo // Reporte Distribucion Canillas (MC005) - Este es un reporte más complejo
Task<( Task<(
IEnumerable<DetalleDistribucionCanillaDto> Canillas, IEnumerable<DetalleDistribucionCanillaDto> Canillas,
IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc, IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc,
IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll, IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll,
IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq, IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq,
IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq, IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq,
IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos, // Para SP_ObtenerCtrlDevoluciones IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos,
IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan, // Para SP_DistCanillasCantidadEntradaSalida IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan,
IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias, // <--- NUEVO para SP_DistCanillasCantidadEntradaSalidaOtrosDias IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias,
string? Error string? Error
)> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa); )> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa, bool? esAccionista);
// Reporte Tiradas por Publicación y Secciones (RR008) // Reporte Tiradas por Publicación y Secciones (RR008)
Task<(IEnumerable<TiradasPublicacionesSeccionesDto> Data, string? Error)> ObtenerTiradasPublicacionesSeccionesAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta, int idPlanta); Task<(IEnumerable<TiradasPublicacionesSeccionesDto> Data, string? Error)> ObtenerTiradasPublicacionesSeccionesAsync(int idPublicacion, DateTime fechaDesde, DateTime fechaHasta, int idPlanta);

View File

@@ -218,30 +218,66 @@ namespace GestionIntegral.Api.Services.Reportes
} }
public async Task<( public async Task<(
IEnumerable<DetalleDistribucionCanillaDto> Canillas, IEnumerable<DetalleDistribucionCanillaDto> Canillas,
IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc, IEnumerable<DetalleDistribucionCanillaDto> CanillasAcc,
IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll, IEnumerable<DetalleDistribucionCanillaAllDto> CanillasAll,
IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq, IEnumerable<DetalleDistribucionCanillaDto> CanillasFechaLiq,
IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq, IEnumerable<DetalleDistribucionCanillaDto> CanillasAccFechaLiq,
IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos, IEnumerable<ObtenerCtrlDevolucionesDto> CtrlDevolucionesRemitos,
IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan, IEnumerable<ControlDevolucionesReporteDto> CtrlDevolucionesParaDistCan,
IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias, IEnumerable<DevueltosOtrosDiasDto> CtrlDevolucionesOtrosDias,
string? Error string? Error
)> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa) )> ObtenerReporteDistribucionCanillasAsync(DateTime fecha, int idEmpresa, bool? esAccionista)
{ {
try try
{ {
var canillasTask = _reportesRepository.GetDetalleDistribucionCanillasPubliAsync(fecha, idEmpresa); // Función helper para convertir fechas a UTC
var canillasAccTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubliAsync(fecha, idEmpresa); Func<IEnumerable<DetalleDistribucionCanillaDto>, IEnumerable<DetalleDistribucionCanillaDto>> toUtc =
items => items?.Select(c => { if (c.Fecha.HasValue) c.Fecha = DateTime.SpecifyKind(c.Fecha.Value.Date, DateTimeKind.Utc); return c; }).ToList()
?? Enumerable.Empty<DetalleDistribucionCanillaDto>();
// --- NUEVA LÓGICA PARA "TODAS LAS EMPRESAS" ---
if (idEmpresa == 0)
{
Task<IEnumerable<DetalleDistribucionCanillaDto>> canillasTask = Task.FromResult(Enumerable.Empty<DetalleDistribucionCanillaDto>());
Task<IEnumerable<DetalleDistribucionCanillaDto>> canillasAccTask = Task.FromResult(Enumerable.Empty<DetalleDistribucionCanillaDto>());
if (esAccionista == true) // Solo accionistas
{
canillasAccTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubli_AllEmpresasAsync(fecha);
}
else // Solo canillitas (o si es null, por defecto canillitas)
{
canillasTask = _reportesRepository.GetDetalleDistribucionCanillasPubli_AllEmpresasAsync(fecha);
}
await Task.WhenAll(canillasTask, canillasAccTask);
return (
toUtc(await canillasTask),
toUtc(await canillasAccTask),
Enumerable.Empty<DetalleDistribucionCanillaAllDto>(), // El resumen no aplica
Enumerable.Empty<DetalleDistribucionCanillaDto>(), // Liquidaciones de otras fechas no aplican en esta vista simplificada
Enumerable.Empty<DetalleDistribucionCanillaDto>(),
Enumerable.Empty<ObtenerCtrlDevolucionesDto>(), // Control de devoluciones no aplica
Enumerable.Empty<ControlDevolucionesReporteDto>(),
Enumerable.Empty<DevueltosOtrosDiasDto>(),
null
);
}
// --- LÓGICA ORIGINAL PARA UNA EMPRESA ESPECÍFICA ---
var canillasTaskOriginal = _reportesRepository.GetDetalleDistribucionCanillasPubliAsync(fecha, idEmpresa);
var canillasAccTaskOriginal = _reportesRepository.GetDetalleDistribucionCanillasAccPubliAsync(fecha, idEmpresa);
var canillasAllTask = _reportesRepository.GetDetalleDistribucionCanillasAllPubliAsync(fecha, idEmpresa); var canillasAllTask = _reportesRepository.GetDetalleDistribucionCanillasAllPubliAsync(fecha, idEmpresa);
var canillasFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasPubliFechaLiqAsync(fecha, idEmpresa); var canillasFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasPubliFechaLiqAsync(fecha, idEmpresa);
var canillasAccFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubliFechaLiqAsync(fecha, idEmpresa); var canillasAccFechaLiqTask = _reportesRepository.GetDetalleDistribucionCanillasAccPubliFechaLiqAsync(fecha, idEmpresa);
var ctrlDevolucionesRemitosTask = _reportesRepository.GetReporteObtenerCtrlDevolucionesAsync(fecha, idEmpresa); // SP_ObtenerCtrlDevoluciones var ctrlDevolucionesRemitosTask = _reportesRepository.GetReporteObtenerCtrlDevolucionesAsync(fecha, idEmpresa);
var ctrlDevolucionesParaDistCanTask = _reportesRepository.GetReporteCtrlDevolucionesParaDistCanAsync(fecha, idEmpresa); // SP_DistCanillasCantidadEntradaSalida var ctrlDevolucionesParaDistCanTask = _reportesRepository.GetReporteCtrlDevolucionesParaDistCanAsync(fecha, idEmpresa);
var ctrlDevolucionesOtrosDiasTask = _reportesRepository.GetEntradaSalidaOtrosDiasAsync(fecha, idEmpresa); // SP_DistCanillasCantidadEntradaSalidaOtrosDias var ctrlDevolucionesOtrosDiasTask = _reportesRepository.GetEntradaSalidaOtrosDiasAsync(fecha, idEmpresa);
await Task.WhenAll( await Task.WhenAll(
canillasTask, canillasAccTask, canillasAllTask, canillasTaskOriginal, canillasAccTaskOriginal, canillasAllTask,
canillasFechaLiqTask, canillasAccFechaLiqTask, canillasFechaLiqTask, canillasAccFechaLiqTask,
ctrlDevolucionesRemitosTask, ctrlDevolucionesParaDistCanTask, ctrlDevolucionesRemitosTask, ctrlDevolucionesParaDistCanTask,
ctrlDevolucionesOtrosDiasTask ctrlDevolucionesOtrosDiasTask
@@ -250,13 +286,9 @@ namespace GestionIntegral.Api.Services.Reportes
var detallesOriginales = await ctrlDevolucionesParaDistCanTask ?? Enumerable.Empty<ControlDevolucionesReporteDto>(); var detallesOriginales = await ctrlDevolucionesParaDistCanTask ?? Enumerable.Empty<ControlDevolucionesReporteDto>();
var detallesOrdenados = detallesOriginales.OrderBy(d => d.Tipo).ToList(); var detallesOrdenados = detallesOriginales.OrderBy(d => d.Tipo).ToList();
Func<IEnumerable<DetalleDistribucionCanillaDto>, IEnumerable<DetalleDistribucionCanillaDto>> toUtc =
items => items?.Select(c => { if (c.Fecha.HasValue) c.Fecha = DateTime.SpecifyKind(c.Fecha.Value.Date, DateTimeKind.Utc); return c; }).ToList()
?? Enumerable.Empty<DetalleDistribucionCanillaDto>();
return ( return (
toUtc(await canillasTask), toUtc(await canillasTaskOriginal),
toUtc(await canillasAccTask), toUtc(await canillasAccTaskOriginal),
await canillasAllTask ?? Enumerable.Empty<DetalleDistribucionCanillaAllDto>(), await canillasAllTask ?? Enumerable.Empty<DetalleDistribucionCanillaAllDto>(),
toUtc(await canillasFechaLiqTask), toUtc(await canillasFechaLiqTask),
toUtc(await canillasAccFechaLiqTask), toUtc(await canillasAccFechaLiqTask),

View File

@@ -17,8 +17,8 @@ namespace GestionIntegral.Api.Services.Suscripciones
private readonly DbConnectionFactory _connectionFactory; private readonly DbConnectionFactory _connectionFactory;
private readonly ILogger<DebitoAutomaticoService> _logger; private readonly ILogger<DebitoAutomaticoService> _logger;
private const string NRO_PRESTACION = "123456"; private const string NRO_PRESTACION = "26435"; // Reemplazar por el número real
private const string ORIGEN_EMPRESA = "ELDIA"; private const string ORIGEN_EMPRESA = "EMPRESA";
public DebitoAutomaticoService( public DebitoAutomaticoService(
IFacturaRepository facturaRepository, IFacturaRepository facturaRepository,
@@ -40,11 +40,9 @@ namespace GestionIntegral.Api.Services.Suscripciones
public async Task<(string? ContenidoArchivo, string? NombreArchivo, string? Error)> GenerarArchivoPagoDirecto(int anio, int mes, int idUsuario) public async Task<(string? ContenidoArchivo, string? NombreArchivo, string? Error)> GenerarArchivoPagoDirecto(int anio, int mes, int idUsuario)
{ {
// Se define la identificación del archivo. // Este número debe ser gestionado para no repetirse. Por ahora, lo mantenemos como 1.
// Este número debe ser gestionado para no repetirse en archivos generados
// para la misma prestación y fecha.
const int identificacionArchivo = 1; const int identificacionArchivo = 1;
var periodo = $"{anio}-{mes:D2}"; var periodo = $"{anio}-{mes:D2}";
var fechaGeneracion = DateTime.Now; var fechaGeneracion = DateTime.Now;
@@ -62,8 +60,6 @@ namespace GestionIntegral.Api.Services.Suscripciones
var importeTotal = facturasParaDebito.Sum(f => f.Factura.ImporteFinal); var importeTotal = facturasParaDebito.Sum(f => f.Factura.ImporteFinal);
var cantidadRegistros = facturasParaDebito.Count(); var cantidadRegistros = facturasParaDebito.Count();
// Se utiliza la variable 'identificacionArchivo' para nombrar el archivo.
var nombreArchivo = $"{NRO_PRESTACION}{fechaGeneracion:yyyyMMdd}{identificacionArchivo}.txt"; var nombreArchivo = $"{NRO_PRESTACION}{fechaGeneracion:yyyyMMdd}{identificacionArchivo}.txt";
var nuevoLote = new LoteDebito var nuevoLote = new LoteDebito
@@ -78,13 +74,11 @@ namespace GestionIntegral.Api.Services.Suscripciones
if (loteCreado == null) throw new DataException("No se pudo crear el registro del lote de débito."); if (loteCreado == null) throw new DataException("No se pudo crear el registro del lote de débito.");
var sb = new StringBuilder(); var sb = new StringBuilder();
// Se pasa la 'identificacionArchivo' al método que crea el Header.
sb.Append(CrearRegistroHeader(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo)); sb.Append(CrearRegistroHeader(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
foreach (var item in facturasParaDebito) foreach (var item in facturasParaDebito)
{ {
sb.Append(CrearRegistroDetalle(item.Factura, item.Suscriptor)); sb.Append(CrearRegistroDetalle(item.Factura, item.Suscriptor));
} }
// Se pasa la 'identificacionArchivo' al método que crea el Trailer.
sb.Append(CrearRegistroTrailer(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo)); sb.Append(CrearRegistroTrailer(fechaGeneracion, importeTotal, cantidadRegistros, identificacionArchivo));
var idsFacturas = facturasParaDebito.Select(f => f.Factura.IdFactura); var idsFacturas = facturasParaDebito.Select(f => f.Factura.IdFactura);
@@ -108,17 +102,18 @@ namespace GestionIntegral.Api.Services.Suscripciones
var facturas = await _facturaRepository.GetByPeriodoAsync(periodo); var facturas = await _facturaRepository.GetByPeriodoAsync(periodo);
var resultado = new List<(Factura, Suscriptor)>(); var resultado = new List<(Factura, Suscriptor)>();
foreach (var f in facturas.Where(fa => fa.EstadoPago == "Pendiente")) // Filtramos por estado Y POR TIPO DE FACTURA
foreach (var f in facturas.Where(fa =>
(fa.EstadoPago == "Pendiente" || fa.EstadoPago == "Pagada Parcialmente" || fa.EstadoPago == "Rechazada") &&
fa.TipoFactura == "Mensual"
))
{ {
var suscriptor = await _suscriptorRepository.GetByIdAsync(f.IdSuscriptor); var suscriptor = await _suscriptorRepository.GetByIdAsync(f.IdSuscriptor);
// Se valida que el CBU de Banelco (22 caracteres) exista antes de intentar la conversión.
if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU) || suscriptor.CBU.Length != 22) if (suscriptor == null || string.IsNullOrWhiteSpace(suscriptor.CBU) || suscriptor.CBU.Length != 22)
{ {
_logger.LogWarning("Suscriptor ID {IdSuscriptor} omitido del lote de débito por CBU inválido o ausente (se esperan 22 dígitos).", suscriptor?.IdSuscriptor); _logger.LogWarning("Suscriptor ID {IdSuscriptor} omitido del lote de débito por CBU inválido o ausente (se esperan 22 dígitos).", f.IdSuscriptor);
continue; continue;
} }
var formaPago = await _formaPagoRepository.GetByIdAsync(suscriptor.IdFormaPagoPreferida); var formaPago = await _formaPagoRepository.GetByIdAsync(suscriptor.IdFormaPagoPreferida);
if (formaPago != null && formaPago.RequiereCBU) if (formaPago != null && formaPago.RequiereCBU)
{ {
@@ -128,26 +123,13 @@ namespace GestionIntegral.Api.Services.Suscripciones
return resultado; return resultado;
} }
// Lógica de conversión de CBU.
private string ConvertirCbuBanelcoASnp(string cbu22) private string ConvertirCbuBanelcoASnp(string cbu22)
{ {
if (string.IsNullOrEmpty(cbu22) || cbu22.Length != 22) if (string.IsNullOrEmpty(cbu22) || cbu22.Length != 22) return "".PadRight(26);
{
_logger.LogError("Se intentó convertir un CBU inválido de {Length} caracteres. Se devolverá un campo vacío.", cbu22?.Length ?? 0);
// Devolver un string de 26 espacios/ceros según la preferencia del banco para campos erróneos.
return "".PadRight(26);
}
// El formato SNP de 26 se obtiene insertando un "0" al inicio y "000" después del 8vo caracter del CBU de 22.
// Formato Banelco (22): [BBBSSSSX] [T....Y]
// Posiciones: (0-7) (8-21)
// Formato SNP (26): 0[BBBSSSSX]000[T....Y]
try try
{ {
string bloque1 = cbu22.Substring(0, 8); // Contiene código de banco, sucursal y DV del bloque 1. string bloque1 = cbu22.Substring(0, 8);
string bloque2 = cbu22.Substring(8); // Contiene el resto de la cadena. string bloque2 = cbu22.Substring(8);
// Reconstruir en formato SNP de 26 dígitos según el instructivo.
return $"0{bloque1}000{bloque2}"; return $"0{bloque1}000{bloque2}";
} }
catch (Exception ex) catch (Exception ex)
@@ -157,9 +139,10 @@ namespace GestionIntegral.Api.Services.Suscripciones
} }
} }
// --- Métodos de Formateo y Mapeo --- // --- Helpers de Formateo ---
private string FormatString(string? value, int length) => (value ?? "").PadRight(length); private string FormatString(string? value, int length) => (value ?? "").PadRight(length);
private string FormatNumeric(long value, int length) => value.ToString().PadLeft(length, '0'); private string FormatNumeric(long value, int length) => value.ToString().PadLeft(length, '0');
private string FormatNumericString(string? value, int length) => (value ?? "").PadLeft(length, '0');
private string MapTipoDocumento(string tipo) => tipo.ToUpper() switch private string MapTipoDocumento(string tipo) => tipo.ToUpper() switch
{ {
"DNI" => "0096", "DNI" => "0096",
@@ -167,17 +150,17 @@ namespace GestionIntegral.Api.Services.Suscripciones
"CUIL" => "0086", "CUIL" => "0086",
"LE" => "0089", "LE" => "0089",
"LC" => "0090", "LC" => "0090",
_ => "0000" // Tipo no especificado o C.I. Policía Federal según anexo. _ => "0000"
}; };
private string CrearRegistroHeader(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo) private string CrearRegistroHeader(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.Append("00"); // Tipo de Registro Header sb.Append("00");
sb.Append(FormatString(NRO_PRESTACION, 6)); sb.Append(FormatNumericString(NRO_PRESTACION, 6));
sb.Append("C"); // Servicio: Sistema Nacional de Pagos sb.Append("C");
sb.Append(fechaGeneracion.ToString("yyyyMMdd")); sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
sb.Append(FormatString(identificacionArchivo.ToString(), 1)); // Identificación de Archivo sb.Append(FormatString(identificacionArchivo.ToString(), 1));
sb.Append(FormatString(ORIGEN_EMPRESA, 7)); sb.Append(FormatString(ORIGEN_EMPRESA, 7));
sb.Append(FormatNumeric((long)(importeTotal * 100), 14)); sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
sb.Append(FormatNumeric(cantidadRegistros, 7)); sb.Append(FormatNumeric(cantidadRegistros, 7));
@@ -188,35 +171,33 @@ namespace GestionIntegral.Api.Services.Suscripciones
private string CrearRegistroDetalle(Factura factura, Suscriptor suscriptor) private string CrearRegistroDetalle(Factura factura, Suscriptor suscriptor)
{ {
// Convertimos el CBU de 22 (Banelco) a 26 (SNP) antes de usarlo.
string cbu26 = ConvertirCbuBanelcoASnp(suscriptor.CBU!); string cbu26 = ConvertirCbuBanelcoASnp(suscriptor.CBU!);
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.Append("0370"); // Tipo de Registro Detalle (Orden de Débito) sb.Append("0370");
sb.Append(FormatString(suscriptor.IdSuscriptor.ToString(), 22)); // Identificación de Cliente sb.Append(FormatString(suscriptor.IdSuscriptor.ToString(), 22));
sb.Append(FormatString(cbu26, 26)); // CBU en formato SNP de 26 caracteres. sb.Append(cbu26);
sb.Append(FormatString($"SUSC-{factura.IdFactura}", 15)); // Referencia Unívoca de la factura. sb.Append(FormatString($"SUSC-{factura.IdFactura}", 15));
sb.Append(factura.FechaVencimiento.ToString("yyyyMMdd")); sb.Append(factura.FechaVencimiento.ToString("yyyyMMdd"));
sb.Append(FormatNumeric((long)(factura.ImporteFinal * 100), 14)); sb.Append(FormatNumeric((long)(factura.ImporteFinal * 100), 14));
sb.Append(FormatNumeric(0, 8)); // Fecha 2do Vencimiento sb.Append(FormatNumeric(0, 8)); // Fecha 2do Vencimiento
sb.Append(FormatNumeric(0, 14)); // Importe 2do Vencimiento sb.Append(FormatNumeric(0, 14)); // Importe 2do Vencimiento
sb.Append(FormatNumeric(0, 8)); // Fecha 3er Vencimiento sb.Append(FormatNumeric(0, 8)); // Fecha 3er Vencimiento
sb.Append(FormatNumeric(0, 14)); // Importe 3er Vencimiento sb.Append(FormatNumeric(0, 14)); // Importe 3er Vencimiento
sb.Append("0"); // Moneda (0 = Pesos) sb.Append("0");
sb.Append(FormatString("", 3)); // Motivo Rechazo (vacío en el envío) sb.Append(FormatString("", 3));
sb.Append(FormatString(MapTipoDocumento(suscriptor.TipoDocumento), 4)); sb.Append(FormatString(MapTipoDocumento(suscriptor.TipoDocumento), 4));
sb.Append(FormatString(suscriptor.NroDocumento, 11)); sb.Append(FormatNumericString(suscriptor.NroDocumento, 11));
sb.Append(FormatString("", 22)); // Nueva ID Cliente sb.Append(FormatString("", 22));
sb.Append(FormatString("", 26)); // Nueva CBU sb.Append(FormatString("", 26));
sb.Append(FormatNumeric(0, 14)); // Importe Mínimo sb.Append(FormatNumeric(0, 14));
sb.Append(FormatNumeric(0, 8)); // Fecha Próximo Vencimiento sb.Append(FormatNumeric(0, 8));
sb.Append(FormatString("", 22)); // Identificación Cuenta Anterior sb.Append(FormatString("", 22));
sb.Append(FormatString("", 40)); // Mensaje ATM sb.Append(FormatString("", 40));
sb.Append(FormatString($"Susc.{factura.Periodo}", 10)); // Concepto Factura sb.Append(FormatString($"Susc.{factura.Periodo}", 10));
sb.Append(FormatNumeric(0, 8)); // Fecha de Cobro sb.Append(FormatNumeric(0, 8));
sb.Append(FormatNumeric(0, 14)); // Importe Cobrado sb.Append(FormatNumeric(0, 14));
sb.Append(FormatNumeric(0, 8)); // Fecha de Acreditamiento sb.Append(FormatNumeric(0, 8));
sb.Append(FormatString("", 26)); // Libre sb.Append(FormatString("", 26));
sb.Append("\r\n"); sb.Append("\r\n");
return sb.ToString(); return sb.ToString();
} }
@@ -224,23 +205,23 @@ namespace GestionIntegral.Api.Services.Suscripciones
private string CrearRegistroTrailer(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo) private string CrearRegistroTrailer(DateTime fechaGeneracion, decimal importeTotal, int cantidadRegistros, int identificacionArchivo)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.Append("99"); // Tipo de Registro Trailer sb.Append("99");
sb.Append(FormatString(NRO_PRESTACION, 6)); sb.Append(FormatNumericString(NRO_PRESTACION, 6));
sb.Append("C"); // Servicio: Sistema Nacional de Pagos sb.Append("C");
sb.Append(fechaGeneracion.ToString("yyyyMMdd")); sb.Append(fechaGeneracion.ToString("yyyyMMdd"));
sb.Append(FormatString(identificacionArchivo.ToString(), 1)); // Identificación de Archivo sb.Append(FormatString(identificacionArchivo.ToString(), 1));
sb.Append(FormatString(ORIGEN_EMPRESA, 7)); sb.Append(FormatString(ORIGEN_EMPRESA, 7));
sb.Append(FormatNumeric((long)(importeTotal * 100), 14)); sb.Append(FormatNumeric((long)(importeTotal * 100), 14));
sb.Append(FormatNumeric(cantidadRegistros, 7)); sb.Append(FormatNumeric(cantidadRegistros, 7));
sb.Append(FormatString("", 304)); sb.Append(FormatString("", 304));
// La última línea del archivo no lleva salto de línea (\r\n). sb.Append("\r\n");
return sb.ToString(); return sb.ToString();
} }
public async Task<ProcesamientoLoteResponseDto> ProcesarArchivoRespuesta(IFormFile archivo, int idUsuario) public async Task<ProcesamientoLoteResponseDto> ProcesarArchivoRespuesta(IFormFile archivo, int idUsuario)
{ {
// Se mantiene la lógica original para procesar el archivo de respuesta del banco. // Se mantiene la lógica original para procesar el archivo de respuesta del banco.
var respuesta = new ProcesamientoLoteResponseDto(); var respuesta = new ProcesamientoLoteResponseDto();
if (archivo == null || archivo.Length == 0) if (archivo == null || archivo.Length == 0)
{ {

View File

@@ -171,10 +171,13 @@ namespace GestionIntegral.Api.Services.Suscripciones
DescuentoAplicado = descuentoPromocionesTotal, DescuentoAplicado = descuentoPromocionesTotal,
ImporteFinal = importeFinal, ImporteFinal = importeFinal,
EstadoPago = "Pendiente", EstadoPago = "Pendiente",
EstadoFacturacion = "Pendiente de Facturar" EstadoFacturacion = "Pendiente de Facturar",
TipoFactura = "Mensual"
}; };
var facturaCreada = await _facturaRepository.CreateAsync(nuevaFactura, transaction); var facturaCreada = await _facturaRepository.CreateAsync(nuevaFactura, transaction);
if (facturaCreada == null) throw new DataException($"No se pudo crear la factura para suscriptor ID {idSuscriptor} y empresa ID {idEmpresa}"); if (facturaCreada == null) throw new DataException($"No se pudo crear la factura para suscriptor ID {idSuscriptor} y empresa ID {idEmpresa}");
facturasCreadas.Add(facturaCreada); facturasCreadas.Add(facturaCreada);
foreach (var detalle in detallesParaFactura) foreach (var detalle in detallesParaFactura)
{ {
@@ -278,11 +281,12 @@ namespace GestionIntegral.Api.Services.Suscripciones
}); });
} }
public async Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion) public async Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(
int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura)
{ {
var periodo = $"{anio}-{mes:D2}"; var periodo = $"{anio}-{mes:D2}";
var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo, nombreSuscriptor, estadoPago, estadoFacturacion); var facturasData = await _facturaRepository.GetByPeriodoEnrichedAsync(periodo, nombreSuscriptor, estadoPago, estadoFacturacion, tipoFactura);
var detallesData = await _facturaDetalleRepository.GetDetallesPorPeriodoAsync(periodo); // Necesitaremos este nuevo método en el repo var detallesData = await _facturaDetalleRepository.GetDetallesPorPeriodoAsync(periodo);
var empresas = await _empresaRepository.GetAllAsync(null, null); var empresas = await _empresaRepository.GetAllAsync(null, null);
var resumenes = facturasData var resumenes = facturasData
@@ -301,10 +305,18 @@ namespace GestionIntegral.Api.Services.Suscripciones
EstadoPago = itemFactura.Factura.EstadoPago, EstadoPago = itemFactura.Factura.EstadoPago,
EstadoFacturacion = itemFactura.Factura.EstadoFacturacion, EstadoFacturacion = itemFactura.Factura.EstadoFacturacion,
NumeroFactura = itemFactura.Factura.NumeroFactura, NumeroFactura = itemFactura.Factura.NumeroFactura,
TotalPagado = itemFactura.TotalPagado,
// Faltaba esta línea para pasar el tipo de factura al frontend.
TipoFactura = itemFactura.Factura.TipoFactura,
Detalles = detallesData Detalles = detallesData
.Where(d => d.IdFactura == itemFactura.Factura.IdFactura) .Where(d => d.IdFactura == itemFactura.Factura.IdFactura)
.Select(d => new FacturaDetalleDto { Descripcion = d.Descripcion, ImporteNeto = d.ImporteNeto }) .Select(d => new FacturaDetalleDto { Descripcion = d.Descripcion, ImporteNeto = d.ImporteNeto })
.ToList() .ToList(),
// Pasamos el id del suscriptor para facilitar las cosas en el frontend
IdSuscriptor = itemFactura.Factura.IdSuscriptor
}; };
}).ToList(); }).ToList();
@@ -314,7 +326,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
NombreSuscriptor = primerItem.NombreSuscriptor, NombreSuscriptor = primerItem.NombreSuscriptor,
Facturas = facturasConsolidadas, Facturas = facturasConsolidadas,
ImporteTotal = facturasConsolidadas.Sum(f => f.ImporteFinal), ImporteTotal = facturasConsolidadas.Sum(f => f.ImporteFinal),
SaldoPendienteTotal = facturasConsolidadas.Sum(f => f.EstadoPago == "Pagada" ? 0 : f.ImporteFinal) SaldoPendienteTotal = facturasConsolidadas.Sum(f => f.ImporteFinal - f.TotalPagado)
}; };
}); });
@@ -578,7 +590,7 @@ namespace GestionIntegral.Api.Services.Suscripciones
} }
} }
private async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction) public async Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction)
{ {
decimal importeTotal = 0; decimal importeTotal = 0;
var diasDeEntrega = suscripcion.DiasEntrega.Split(',').ToHashSet(); var diasDeEntrega = suscripcion.DiasEntrega.Split(',').ToHashSet();

View File

@@ -1,5 +1,7 @@
using System.Data;
using GestionIntegral.Api.Dtos.Comunicaciones; using GestionIntegral.Api.Dtos.Comunicaciones;
using GestionIntegral.Api.Dtos.Suscripciones; using GestionIntegral.Api.Dtos.Suscripciones;
using GestionIntegral.Api.Models.Suscripciones;
namespace GestionIntegral.Api.Services.Suscripciones namespace GestionIntegral.Api.Services.Suscripciones
{ {
@@ -7,8 +9,10 @@ namespace GestionIntegral.Api.Services.Suscripciones
{ {
Task<(bool Exito, string? Mensaje, LoteDeEnvioResumenDto? ResultadoEnvio)> GenerarFacturacionMensual(int anio, int mes, int idUsuario); Task<(bool Exito, string? Mensaje, LoteDeEnvioResumenDto? ResultadoEnvio)> GenerarFacturacionMensual(int anio, int mes, int idUsuario);
Task<IEnumerable<LoteDeEnvioHistorialDto>> ObtenerHistorialLotesEnvio(int? anio, int? mes); Task<IEnumerable<LoteDeEnvioHistorialDto>> ObtenerHistorialLotesEnvio(int? anio, int? mes);
Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion); Task<IEnumerable<ResumenCuentaSuscriptorDto>> ObtenerResumenesDeCuentaPorPeriodo(
int anio, int mes, string? nombreSuscriptor, string? estadoPago, string? estadoFacturacion, string? tipoFactura);
Task<(bool Exito, string? Error, string? EmailDestino)> EnviarFacturaPdfPorEmail(int idFactura, int idUsuario); Task<(bool Exito, string? Error, string? EmailDestino)> EnviarFacturaPdfPorEmail(int idFactura, int idUsuario);
Task<(bool Exito, string? Error)> ActualizarNumeroFactura(int idFactura, string numeroFactura, int idUsuario); Task<(bool Exito, string? Error)> ActualizarNumeroFactura(int idFactura, string numeroFactura, int idUsuario);
Task<decimal> CalcularImporteParaSuscripcion(Suscripcion suscripcion, int anio, int mes, IDbTransaction transaction);
} }
} }

View File

@@ -70,14 +70,11 @@ namespace GestionIntegral.Api.Services.Suscripciones
{ {
var factura = await _facturaRepository.GetByIdAsync(createDto.IdFactura); var factura = await _facturaRepository.GetByIdAsync(createDto.IdFactura);
if (factura == null) return (null, "La factura especificada no existe."); if (factura == null) return (null, "La factura especificada no existe.");
// Usar EstadoPago para la validación
if (factura.EstadoPago == "Anulada") return (null, "No se puede registrar un pago sobre una factura anulada."); if (factura.EstadoPago == "Anulada") return (null, "No se puede registrar un pago sobre una factura anulada.");
var formaPago = await _formaPagoRepository.GetByIdAsync(createDto.IdFormaPago); var formaPago = await _formaPagoRepository.GetByIdAsync(createDto.IdFormaPago);
if (formaPago == null || !formaPago.Activo) return (null, "La forma de pago no es válida."); if (formaPago == null || !formaPago.Activo) return (null, "La forma de pago no es válida.");
// Obtenemos la suma de pagos ANTERIORES
var totalPagadoAnteriormente = await _pagoRepository.GetTotalPagadoAprobadoAsync(createDto.IdFactura, transaction); var totalPagadoAnteriormente = await _pagoRepository.GetTotalPagadoAprobadoAsync(createDto.IdFactura, transaction);
var nuevoPago = new Pago var nuevoPago = new Pago
@@ -96,37 +93,31 @@ namespace GestionIntegral.Api.Services.Suscripciones
var pagoCreado = await _pagoRepository.CreateAsync(nuevoPago, transaction); var pagoCreado = await _pagoRepository.CreateAsync(nuevoPago, transaction);
if (pagoCreado == null) throw new DataException("No se pudo registrar el pago."); if (pagoCreado == null) throw new DataException("No se pudo registrar el pago.");
// Calculamos el nuevo total EN MEMORIA
var nuevoTotalPagado = totalPagadoAnteriormente + pagoCreado.Monto; var nuevoTotalPagado = totalPagadoAnteriormente + pagoCreado.Monto;
// Comparamos y actualizamos el estado si es necesario // Nueva lógica para manejar todos los estados de pago
// CORRECCIÓN: Usar EstadoPago y el método correcto del repositorio string nuevoEstadoPago = factura.EstadoPago;
if (factura.EstadoPago != "Pagada" && nuevoTotalPagado >= factura.ImporteFinal) if (nuevoTotalPagado >= factura.ImporteFinal)
{ {
bool actualizado = await _facturaRepository.UpdateEstadoPagoAsync(factura.IdFactura, "Pagada", transaction); nuevoEstadoPago = "Pagada";
if (!actualizado) throw new DataException("No se pudo actualizar el estado de la factura a 'Pagada'."); }
else if (nuevoTotalPagado > 0)
{
nuevoEstadoPago = "Pagada Parcialmente";
}
// Si nuevoTotalPagado es 0, el estado no cambia.
// Solo actualizamos si el estado calculado es diferente al actual.
if (nuevoEstadoPago != factura.EstadoPago)
{
bool actualizado = await _facturaRepository.UpdateEstadoPagoAsync(factura.IdFactura, nuevoEstadoPago, transaction);
if (!actualizado) throw new DataException($"No se pudo actualizar el estado de la factura a '{nuevoEstadoPago}'.");
} }
transaction.Commit(); transaction.Commit();
_logger.LogInformation("Pago manual ID {IdPago} registrado para Factura ID {IdFactura} por Usuario ID {IdUsuario}", pagoCreado.IdPago, pagoCreado.IdFactura, idUsuario); _logger.LogInformation("Pago manual ID {IdPago} registrado para Factura ID {IdFactura} por Usuario ID {IdUsuario}", pagoCreado.IdPago, pagoCreado.IdFactura, idUsuario);
// Construimos el DTO de respuesta SIN volver a consultar la base de datos var dto = await MapToDto(pagoCreado); // MapToDto ahora es más simple
var usuario = await _usuarioRepository.GetByIdAsync(idUsuario);
var dto = new PagoDto
{
IdPago = pagoCreado.IdPago,
IdFactura = pagoCreado.IdFactura,
FechaPago = pagoCreado.FechaPago.ToString("yyyy-MM-dd"),
IdFormaPago = pagoCreado.IdFormaPago,
NombreFormaPago = formaPago.Nombre,
Monto = pagoCreado.Monto,
Estado = pagoCreado.Estado,
Referencia = pagoCreado.Referencia,
Observaciones = pagoCreado.Observaciones,
IdUsuarioRegistro = pagoCreado.IdUsuarioRegistro,
NombreUsuarioRegistro = usuario != null ? $"{usuario.Nombre} {usuario.Apellido}" : "N/A"
};
return (dto, null); return (dto, null);
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -3,12 +3,8 @@ using GestionIntegral.Api.Data.Repositories.Distribucion;
using GestionIntegral.Api.Data.Repositories.Suscripciones; using GestionIntegral.Api.Data.Repositories.Suscripciones;
using GestionIntegral.Api.Dtos.Suscripciones; using GestionIntegral.Api.Dtos.Suscripciones;
using GestionIntegral.Api.Models.Suscripciones; using GestionIntegral.Api.Models.Suscripciones;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data; using System.Data;
using System.Linq; using System.Globalization;
using System.Threading.Tasks;
namespace GestionIntegral.Api.Services.Suscripciones namespace GestionIntegral.Api.Services.Suscripciones
{ {
@@ -18,23 +14,32 @@ namespace GestionIntegral.Api.Services.Suscripciones
private readonly ISuscriptorRepository _suscriptorRepository; private readonly ISuscriptorRepository _suscriptorRepository;
private readonly IPublicacionRepository _publicacionRepository; private readonly IPublicacionRepository _publicacionRepository;
private readonly IPromocionRepository _promocionRepository; private readonly IPromocionRepository _promocionRepository;
private readonly DbConnectionFactory _connectionFactory; private readonly IFacturaRepository _facturaRepository;
private readonly IFacturaDetalleRepository _facturaDetalleRepository;
private readonly IFacturacionService _facturacionService;
private readonly ILogger<SuscripcionService> _logger; private readonly ILogger<SuscripcionService> _logger;
private readonly DbConnectionFactory _connectionFactory;
public SuscripcionService( public SuscripcionService(
ISuscripcionRepository suscripcionRepository, ISuscripcionRepository suscripcionRepository,
ISuscriptorRepository suscriptorRepository, ISuscriptorRepository suscriptorRepository,
IPublicacionRepository publicacionRepository, IPublicacionRepository publicacionRepository,
IPromocionRepository promocionRepository, IPromocionRepository promocionRepository,
DbConnectionFactory connectionFactory, IFacturaRepository facturaRepository,
ILogger<SuscripcionService> logger) IFacturaDetalleRepository facturaDetalleRepository,
IFacturacionService facturacionService,
ILogger<SuscripcionService> logger,
DbConnectionFactory connectionFactory)
{ {
_suscripcionRepository = suscripcionRepository; _suscripcionRepository = suscripcionRepository;
_suscriptorRepository = suscriptorRepository; _suscriptorRepository = suscriptorRepository;
_publicacionRepository = publicacionRepository; _publicacionRepository = publicacionRepository;
_promocionRepository = promocionRepository; _promocionRepository = promocionRepository;
_connectionFactory = connectionFactory; _facturaRepository = facturaRepository;
_facturaDetalleRepository = facturaDetalleRepository;
_facturacionService = facturacionService;
_logger = logger; _logger = logger;
_connectionFactory = connectionFactory;
} }
private PromocionDto MapPromocionToDto(Promocion promo) => new PromocionDto private PromocionDto MapPromocionToDto(Promocion promo) => new PromocionDto
@@ -122,6 +127,53 @@ namespace GestionIntegral.Api.Services.Suscripciones
var creada = await _suscripcionRepository.CreateAsync(nuevaSuscripcion, transaction); var creada = await _suscripcionRepository.CreateAsync(nuevaSuscripcion, transaction);
if (creada == null) throw new DataException("Error al crear la suscripción."); if (creada == null) throw new DataException("Error al crear la suscripción.");
var ultimoPeriodoFacturadoStr = await _facturaRepository.GetUltimoPeriodoFacturadoAsync();
if (ultimoPeriodoFacturadoStr != null)
{
var ultimoPeriodo = DateTime.ParseExact(ultimoPeriodoFacturadoStr, "yyyy-MM", CultureInfo.InvariantCulture);
var periodoSuscripcion = new DateTime(creada.FechaInicio.Year, creada.FechaInicio.Month, 1);
if (periodoSuscripcion <= ultimoPeriodo)
{
_logger.LogInformation("Suscripción en período ya cerrado detectada. Generando factura de alta pro-rata.");
decimal importeProporcional = await _facturacionService.CalcularImporteParaSuscripcion(creada, creada.FechaInicio.Year, creada.FechaInicio.Month, transaction);
if (importeProporcional > 0)
{
var facturaDeAlta = new Factura
{
IdSuscriptor = creada.IdSuscriptor,
Periodo = creada.FechaInicio.ToString("yyyy-MM"),
FechaEmision = DateTime.Now.Date,
FechaVencimiento = DateTime.Now.AddDays(10).Date,
ImporteBruto = importeProporcional,
ImporteFinal = importeProporcional,
EstadoPago = "Pendiente",
EstadoFacturacion = "Pendiente de Facturar",
TipoFactura = "Alta"
};
var facturaCreada = await _facturaRepository.CreateAsync(facturaDeAlta, transaction);
if (facturaCreada == null) throw new DataException("No se pudo crear la factura de alta.");
var publicacion = await _publicacionRepository.GetByIdSimpleAsync(creada.IdPublicacion);
var finDeMes = new DateTime(creada.FechaInicio.Year, creada.FechaInicio.Month, 1).AddMonths(1).AddDays(-1);
await _facturaDetalleRepository.CreateAsync(new FacturaDetalle
{
IdFactura = facturaCreada.IdFactura,
IdSuscripcion = creada.IdSuscripcion,
Descripcion = $"Suscripción proporcional {publicacion?.Nombre} ({creada.FechaInicio:dd/MM} al {finDeMes:dd/MM})",
ImporteBruto = importeProporcional,
ImporteNeto = importeProporcional,
DescuentoAplicado = 0
}, transaction);
_logger.LogInformation("Factura de alta #{IdFactura} por ${Importe} generada para la nueva suscripción #{IdSuscripcion}.", facturaCreada.IdFactura, importeProporcional, creada.IdSuscripcion);
}
}
}
transaction.Commit(); transaction.Commit();
_logger.LogInformation("Suscripción ID {Id} creada por Usuario ID {UserId}.", creada.IdSuscripcion, idUsuario); _logger.LogInformation("Suscripción ID {Id} creada por Usuario ID {UserId}.", creada.IdSuscripcion, idUsuario);
return (await MapToDto(creada), null); return (await MapToDto(creada), null);

View File

@@ -16,11 +16,11 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"MailSettings": { "MailSettings": {
"SmtpHost": "", "SmtpHost": "192.168.5.201",
"SmtpPort": 0, "SmtpPort": 587,
"SenderName": "", "SenderName": "Club - Diario El Día",
"SenderEmail": "", "SenderEmail": "alertas@eldia.com",
"SmtpUser": "", "SmtpUser": "alertas@eldia.com",
"SmtpPass": "" "SmtpPass": "@Alertas713550@"
} }
} }

View File

@@ -64,6 +64,7 @@ const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
const isEditing = Boolean(initialData); const isEditing = Boolean(initialData);
useEffect(() => { useEffect(() => {
// Esta función se encarga de cargar los datos de los dropdowns.
const fetchDropdownData = async () => { const fetchDropdownData = async () => {
setLoadingDropdowns(true); setLoadingDropdowns(true);
try { try {
@@ -126,14 +127,16 @@ const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
setLoading(true); setLoading(true);
try { try {
const montoNum = parseFloat(monto); const montoNum = parseFloat(monto);
if (isEditing && initialData) { if (isEditing && initialData) {
const dataToSubmit: UpdatePagoDistribuidorDto = { const dataToSubmit: UpdatePagoDistribuidorDto = {
monto: montoNum, monto: montoNum,
idTipoPago: Number(idTipoPago), idTipoPago: Number(idTipoPago),
detalle: detalle || undefined, detalle: detalle || undefined,
}; };
// << INICIO DE LA CORRECCIÓN >>
await onSubmit(dataToSubmit, initialData.idPago); await onSubmit(dataToSubmit, initialData.idPago);
// << FIN DE LA CORRECCIÓN >>
} else { } else {
const dataToSubmit: CreatePagoDistribuidorDto = { const dataToSubmit: CreatePagoDistribuidorDto = {
idDistribuidor: Number(idDistribuidor), idDistribuidor: Number(idDistribuidor),
@@ -147,7 +150,9 @@ const PagoDistribuidorFormModal: React.FC<PagoDistribuidorFormModalProps> = ({
}; };
await onSubmit(dataToSubmit); await onSubmit(dataToSubmit);
} }
onClose(); onClose();
} catch (error: any) { } catch (error: any) {
console.error("Error en submit de PagoDistribuidorFormModal:", error); console.error("Error en submit de PagoDistribuidorFormModal:", error);
} finally { } finally {

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

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent, InputAdornment } from '@mui/material'; import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert, FormControl, InputLabel, Select, MenuItem, type SelectChangeEvent, InputAdornment } from '@mui/material';
import type { FacturaDto } from '../../../models/dtos/Suscripciones/FacturaDto'; import type { FacturaConsolidadaDto } from '../../../models/dtos/Suscripciones/ResumenCuentaSuscriptorDto';
import type { CreatePagoDto } from '../../../models/dtos/Suscripciones/CreatePagoDto'; import type { CreatePagoDto } from '../../../models/dtos/Suscripciones/CreatePagoDto';
import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto'; import type { FormaPagoDto } from '../../../models/dtos/Suscripciones/FormaPagoDto';
import formaPagoService from '../../../services/Suscripciones/formaPagoService'; import formaPagoService from '../../../services/Suscripciones/formaPagoService';
@@ -23,17 +23,19 @@ interface PagoManualModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSubmit: (data: CreatePagoDto) => Promise<void>; onSubmit: (data: CreatePagoDto) => Promise<void>;
factura: FacturaDto | null; factura: FacturaConsolidadaDto | null;
nombreSuscriptor: string; // Se pasa el nombre del suscriptor como prop-
errorMessage?: string | null; errorMessage?: string | null;
clearErrorMessage: () => void; clearErrorMessage: () => void;
} }
const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, errorMessage, clearErrorMessage }) => { const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubmit, factura, nombreSuscriptor, errorMessage, clearErrorMessage }) => {
const [formData, setFormData] = useState<Partial<CreatePagoDto>>({}); const [formData, setFormData] = useState<Partial<CreatePagoDto>>({});
const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]); const [formasDePago, setFormasDePago] = useState<FormaPagoDto[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingFormasPago, setLoadingFormasPago] = useState(false); const [loadingFormasPago, setLoadingFormasPago] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
const saldoPendiente = factura ? factura.importeFinal - factura.totalPagado : 0;
useEffect(() => { useEffect(() => {
const fetchFormasDePago = async () => { const fetchFormasDePago = async () => {
@@ -52,26 +54,24 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
fetchFormasDePago(); fetchFormasDePago();
setFormData({ setFormData({
idFactura: factura.idFactura, idFactura: factura.idFactura,
monto: factura.saldoPendiente, // Usar el saldo pendiente como valor por defecto monto: saldoPendiente,
fechaPago: new Date().toISOString().split('T')[0] fechaPago: new Date().toISOString().split('T')[0]
}); });
setLocalErrors({}); setLocalErrors({});
} }
}, [open, factura]); }, [open, factura, saldoPendiente]);
const validate = (): boolean => { const validate = (): boolean => {
const errors: { [key: string]: string | null } = {}; const errors: { [key: string]: string | null } = {};
if (!formData.idFormaPago) errors.idFormaPago = "Seleccione una forma de pago."; if (!formData.idFormaPago) errors.idFormaPago = "Seleccione una forma de pago.";
if (!formData.fechaPago) errors.fechaPago = "La fecha de pago es obligatoria."; if (!formData.fechaPago) errors.fechaPago = "La fecha de pago es obligatoria.";
const monto = formData.monto ?? 0; const monto = formData.monto ?? 0;
const saldo = factura?.saldoPendiente ?? 0;
if (monto <= 0) { if (monto <= 0) {
errors.monto = "El monto debe ser mayor a cero."; errors.monto = "El monto debe ser mayor a cero.";
} else if (monto > saldo) { } else if (monto > saldoPendiente) {
// Usamos toFixed(2) para mostrar el formato de moneda correcto en el mensaje errors.monto = `El monto no puede superar el saldo pendiente de $${saldoPendiente.toFixed(2)}.`;
errors.monto = `El monto no puede superar el saldo pendiente de $${saldo.toFixed(2)}.`;
} }
setLocalErrors(errors); setLocalErrors(errors);
@@ -85,7 +85,7 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null })); if (localErrors[name]) setLocalErrors(prev => ({ ...prev, [name]: null }));
if (errorMessage) clearErrorMessage(); if (errorMessage) clearErrorMessage();
}; };
const handleSelectChange = (e: SelectChangeEvent<any>) => { const handleSelectChange = (e: SelectChangeEvent<any>) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value })); setFormData(prev => ({ ...prev, [name]: value }));
@@ -117,29 +117,32 @@ const PagoManualModal: React.FC<PagoManualModalProps> = ({ open, onClose, onSubm
<Modal open={open} onClose={onClose}> <Modal open={open} onClose={onClose}>
<Box sx={modalStyle}> <Box sx={modalStyle}>
<Typography variant="h6">Registrar Pago Manual</Typography> <Typography variant="h6">Registrar Pago Manual</Typography>
<Typography variant="subtitle1" gutterBottom sx={{fontWeight: 'bold'}}> <Typography variant="body1" color="text.secondary" gutterBottom>
Saldo Pendiente: ${factura.saldoPendiente.toFixed(2)} Para: {nombreSuscriptor}
</Typography>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 'bold' }}>
Saldo Pendiente: ${saldoPendiente.toFixed(2)}
</Typography> </Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}> <Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
<TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} /> <TextField name="fechaPago" label="Fecha de Pago" type="date" value={formData.fechaPago || ''} onChange={handleInputChange} required fullWidth margin="dense" InputLabelProps={{ shrink: true }} error={!!localErrors.fechaPago} helperText={localErrors.fechaPago} />
<FormControl fullWidth margin="dense" error={!!localErrors.idFormaPago}> <FormControl fullWidth margin="dense" error={!!localErrors.idFormaPago}>
<InputLabel id="forma-pago-label" required>Forma de Pago</InputLabel> <InputLabel id="forma-pago-label" required>Forma de Pago</InputLabel>
<Select name="idFormaPago" labelId="forma-pago-label" value={formData.idFormaPago || ''} onChange={handleSelectChange} label="Forma de Pago" disabled={loadingFormasPago}> <Select name="idFormaPago" labelId="forma-pago-label" value={formData.idFormaPago || ''} onChange={handleSelectChange} label="Forma de Pago" disabled={loadingFormasPago}>
{formasDePago.map(fp => <MenuItem key={fp.idFormaPago} value={fp.idFormaPago}>{fp.nombre}</MenuItem>)} {formasDePago.map(fp => <MenuItem key={fp.idFormaPago} value={fp.idFormaPago}>{fp.nombre}</MenuItem>)}
</Select> </Select>
</FormControl> </FormControl>
<TextField name="monto" label="Monto Pagado" type="number" value={formData.monto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.monto} helperText={localErrors.monto} InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} inputProps={{ step: "0.01" }} /> <TextField name="monto" label="Monto Pagado" type="number" value={formData.monto || ''} onChange={handleInputChange} required fullWidth margin="dense" error={!!localErrors.monto} helperText={localErrors.monto} InputProps={{ startAdornment: <InputAdornment position="start">$</InputAdornment> }} inputProps={{ step: "0.01" }} />
<TextField name="referencia" label="Referencia (Opcional)" value={formData.referencia || ''} onChange={handleInputChange} fullWidth margin="dense" /> <TextField name="referencia" label="Referencia (Opcional)" value={formData.referencia || ''} onChange={handleInputChange} fullWidth margin="dense" />
<TextField name="observaciones" label="Observaciones (Opcional)" value={formData.observaciones || ''} onChange={handleInputChange} fullWidth margin="dense" multiline rows={2} /> <TextField name="observaciones" label="Observaciones (Opcional)" value={formData.observaciones || ''} onChange={handleInputChange} fullWidth margin="dense" multiline rows={2} />
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>} {errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}> <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button> <Button onClick={onClose} color="secondary" disabled={loading}>Cancelar</Button>
<Button type="submit" variant="contained" disabled={loading || loadingFormasPago}> <Button type="submit" variant="contained" disabled={loading || loadingFormasPago}>
{loading ? <CircularProgress size={24} /> : 'Registrar Pago'} {loading ? <CircularProgress size={24} /> : 'Registrar Pago'}
</Button> </Button>
</Box> </Box>
</Box> </Box>
</Box> </Box>
</Modal> </Modal>

View File

@@ -13,19 +13,19 @@ const SECCION_PERMISSIONS_PREFIX = "SS";
// Mapeo de codAcc de sección a su módulo conceptual // Mapeo de codAcc de sección a su módulo conceptual
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => { const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
if (codAcc === "SS001") return "Distribución"; if (codAcc === "SS001") return "Distribución";
if (codAcc === "SS007") return "Suscripciones";
if (codAcc === "SS002") return "Contables"; if (codAcc === "SS002") return "Contables";
if (codAcc === "SS003") return "Impresión"; if (codAcc === "SS003") return "Impresión";
if (codAcc === "SS004") return "Reportes"; if (codAcc === "SS004") return "Reportes";
if (codAcc === "SS006") return "Usuarios";
if (codAcc === "SS005") return "Radios"; if (codAcc === "SS005") return "Radios";
if (codAcc === "SS006") return "Usuarios";
return null; return null;
}; };
// Función para determinar el módulo conceptual de un permiso individual // Función para determinar el módulo conceptual de un permiso individual
const getModuloConceptualDelPermiso = (permisoModulo: string): string => { const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
const moduloLower = permisoModulo.toLowerCase(); const moduloLower = permisoModulo.toLowerCase();
if (moduloLower.includes("distribuidores") || if (moduloLower.includes("distribuidores") ||
moduloLower.includes("canillas") || // Cubre "Canillas" y "Movimientos Canillas" moduloLower.includes("canillas") || // Cubre "Canillas" y "Movimientos Canillas"
moduloLower.includes("publicaciones distribución") || moduloLower.includes("publicaciones distribución") ||
@@ -36,6 +36,9 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
moduloLower.includes("ctrl. devoluciones")) { moduloLower.includes("ctrl. devoluciones")) {
return "Distribución"; return "Distribución";
} }
if (moduloLower.includes("suscripciones")) {
return "Suscripciones";
}
if (moduloLower.includes("cuentas pagos") || if (moduloLower.includes("cuentas pagos") ||
moduloLower.includes("cuentas notas") || moduloLower.includes("cuentas notas") ||
moduloLower.includes("cuentas tipos pagos")) { moduloLower.includes("cuentas tipos pagos")) {
@@ -89,7 +92,7 @@ const PermisosChecklist: React.FC<PermisosChecklistProps> = ({
return acc; return acc;
}, {} as Record<string, PermisoAsignadoDto[]>); }, {} as Record<string, PermisoAsignadoDto[]>);
const ordenModulosPrincipales = ["Distribución", "Contables", "Impresión", "Radios", "Usuarios", "Reportes", "Permisos (Definición)"]; const ordenModulosPrincipales = ["Distribución", "Suscripciones", "Contables", "Impresión", "Usuarios", "Reportes", "Radios","Permisos (Definición)"];
// Añadir módulos que solo tienen permiso de sección (como Radios) pero no hijos (aún) // Añadir módulos que solo tienen permiso de sección (como Radios) pero no hijos (aún)
permisosDeSeccion.forEach(ps => { permisosDeSeccion.forEach(ps => {
const moduloConceptual = getModuloFromSeccionCodAcc(ps.codAcc); const moduloConceptual = getModuloFromSeccionCodAcc(ps.codAcc);

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { import { Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
Modal, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox FormControl, InputLabel, Select, MenuItem, FormControlLabel, Checkbox
} from '@mui/material'; } from '@mui/material';
import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto'; import type { UsuarioDto } from '../../../models/dtos/Usuarios/UsuarioDto';

View File

@@ -1,22 +1,22 @@
// src/hooks/usePermissions.ts
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { useCallback } from 'react';
export const usePermissions = () => { export const usePermissions = () => {
const { user } = useAuth(); // user aquí es de tipo UserContextData | null const { user } = useAuth();
const tienePermiso = (codigoPermisoRequerido: string): boolean => { // Envolvemos la función en useCallback.
if (!user) { // Si no hay usuario logueado // Su dependencia es [user], por lo que la función solo se
// volverá a crear si el objeto 'user' cambia (ej. al iniciar/cerrar sesión).
const tienePermiso = useCallback((codigoPermisoRequerido: string): boolean => {
if (!user) {
return false; return false;
} }
if (user.esSuperAdmin) { // SuperAdmin tiene todos los permisos if (user.esSuperAdmin) {
return true; return true;
} }
// Verificar si la lista de permisos del usuario incluye el código requerido
return user.permissions?.includes(codigoPermisoRequerido) ?? false; return user.permissions?.includes(codigoPermisoRequerido) ?? false;
}; }, [user]);
// También puede exportar el objeto user completo si se necesita en otros lugares
// o propiedades específicas como idPerfil, esSuperAdmin.
return { return {
tienePermiso, tienePermiso,
isSuperAdmin: user?.esSuperAdmin ?? false, isSuperAdmin: user?.esSuperAdmin ?? false,

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

@@ -12,6 +12,8 @@ export interface FacturaConsolidadaDto {
estadoPago: string; estadoPago: string;
estadoFacturacion: string; estadoFacturacion: string;
numeroFactura?: string | null; numeroFactura?: string | null;
totalPagado: number;
tipoFactura: 'Mensual' | 'Alta';
detalles: FacturaDetalleDto[]; detalles: FacturaDetalleDto[];
// Añadimos el id del suscriptor para que sea fácil pasarlo a los handlers // Añadimos el id del suscriptor para que sea fácil pasarlo a los handlers
idSuscriptor: number; idSuscriptor: number;

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } 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, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip CircularProgress, Alert, FormControl, InputLabel, Select, Tooltip
} from '@mui/material'; } from '@mui/material';
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';
@@ -28,11 +28,11 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
const [pagos, setPagos] = useState<PagoDistribuidorDto[]>([]); const [pagos, setPagos] = useState<PagoDistribuidorDto[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); const [pageApiErrorMessage, setPageApiErrorMessage] = useState<string | null>(null);
const [modalApiErrorMessage, setModalApiErrorMessage] = useState<string | null>(null);
// Filtros const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState(''); const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);//useState('');
const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>(''); const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>('');
const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>(''); const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>('');
const [filtroTipoMov, setFiltroTipoMov] = useState<'Recibido' | 'Realizado' | ''>(''); const [filtroTipoMov, setFiltroTipoMov] = useState<'Recibido' | 'Realizado' | ''>('');
@@ -50,7 +50,6 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
const [selectedRow, setSelectedRow] = useState<PagoDistribuidorDto | null>(null); const [selectedRow, setSelectedRow] = useState<PagoDistribuidorDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions(); const { tienePermiso, isSuperAdmin } = usePermissions();
// Permisos CP001 (Ver), CP002 (Crear), CP003 (Modificar), CP004 (Eliminar)
const puedeVer = isSuperAdmin || tienePermiso("CP001"); const puedeVer = isSuperAdmin || tienePermiso("CP001");
const puedeCrear = isSuperAdmin || tienePermiso("CP002"); const puedeCrear = isSuperAdmin || tienePermiso("CP002");
const puedeModificar = isSuperAdmin || tienePermiso("CP003"); const puedeModificar = isSuperAdmin || tienePermiso("CP003");
@@ -59,21 +58,27 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
const fetchFiltersDropdownData = useCallback(async () => { const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true); setLoadingFiltersDropdown(true);
try { try {
const [distData, empData] = await Promise.all([ const [distData, empData] = await Promise.all([
distribuidorService.getAllDistribuidores(), distribuidorService.getAllDistribuidores(),
empresaService.getAllEmpresas() empresaService.getAllEmpresas()
]); ]);
setDistribuidores(distData); setDistribuidores(distData);
setEmpresas(empData); setEmpresas(empData);
} catch (err) { console.error(err); setError("Error al cargar opciones de filtro."); } catch (err) {
console.error(err); setError("Error al cargar opciones de filtro.");
} finally { setLoadingFiltersDropdown(false); } } finally { setLoadingFiltersDropdown(false); }
}, []); }, []);
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
const clearModalApiErrorMessage = useCallback(() => {
setModalApiErrorMessage(null);
}, []);
const cargarPagos = useCallback(async () => { const cargarPagos = useCallback(async () => {
if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; } if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; }
setLoading(true); setError(null); setApiErrorMessage(null); setLoading(true); setError(null); setPageApiErrorMessage(null);
try { try {
const params = { const params = {
fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null, fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null,
@@ -83,19 +88,20 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
}; };
const data = await pagoDistribuidorService.getAllPagosDistribuidor(params); const data = await pagoDistribuidorService.getAllPagosDistribuidor(params);
setPagos(data); setPagos(data);
} catch (err) { console.error(err); setError('Error al cargar los pagos.'); } catch (err) {
console.error(err); setError('Error al cargar los pagos.');
} finally { setLoading(false); } } finally { setLoading(false); }
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdDistribuidor, filtroIdEmpresa, filtroTipoMov]); }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdDistribuidor, filtroIdEmpresa, filtroTipoMov]);
useEffect(() => { cargarPagos(); }, [cargarPagos]); useEffect(() => { cargarPagos(); }, [cargarPagos]);
const handleOpenModal = (item?: PagoDistribuidorDto) => { const handleOpenModal = (item?: PagoDistribuidorDto) => {
setEditingPago(item || null); setApiErrorMessage(null); setModalOpen(true); setEditingPago(item || null); setModalApiErrorMessage(null); setModalOpen(true);
}; };
const handleCloseModal = () => { setModalOpen(false); setEditingPago(null); }; const handleCloseModal = () => { setModalOpen(false); setEditingPago(null); };
const handleSubmitModal = async (data: CreatePagoDistribuidorDto | UpdatePagoDistribuidorDto, idPago?: number) => { const handleSubmitModal = async (data: CreatePagoDistribuidorDto | UpdatePagoDistribuidorDto, idPago?: number) => {
setApiErrorMessage(null); setModalApiErrorMessage(null);
try { try {
if (idPago && editingPago) { if (idPago && editingPago) {
await pagoDistribuidorService.updatePagoDistribuidor(idPago, data as UpdatePagoDistribuidorDto); await pagoDistribuidorService.updatePagoDistribuidor(idPago, data as UpdatePagoDistribuidorDto);
@@ -105,15 +111,19 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
cargarPagos(); cargarPagos();
} catch (err: any) { } catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el pago.'; const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el pago.';
setApiErrorMessage(message); throw err; setModalApiErrorMessage(message);
throw err;
} }
}; };
const handleDelete = async (idPago: number) => { const handleDelete = async (idPago: number) => {
if (window.confirm(`¿Seguro de eliminar este pago (ID: ${idPago})? Esta acción revertirá el impacto en el saldo.`)) { if (window.confirm(`¿Seguro de eliminar este pago (ID: ${idPago})? Esta acción revertirá el impacto en el saldo.`)) {
setApiErrorMessage(null); setPageApiErrorMessage(null);
try { await pagoDistribuidorService.deletePagoDistribuidor(idPago); cargarPagos(); } try { await pagoDistribuidorService.deletePagoDistribuidor(idPago); cargarPagos(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); } catch (err: any) {
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.';
setPageApiErrorMessage(msg);
}
} }
handleMenuClose(); handleMenuClose();
}; };
@@ -128,7 +138,15 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
}; };
const displayData = pagos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); const displayData = pagos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-'; const formatDate = (dateString?: string | null): string => {
if (!dateString) return '-';
const datePart = dateString.split('T')[0];
const parts = datePart.split('-');
if (parts.length === 3) {
return `${parts[2]}/${parts[1]}/${parts[0]}`;
}
return datePart;
};
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
@@ -136,93 +154,96 @@ const GestionarPagosDistribuidorPage: React.FC = () => {
<Box sx={{ p: 1 }}> <Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Pagos de Distribuidores</Typography> <Typography variant="h5" gutterBottom>Pagos de Distribuidores</Typography>
<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 Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> <TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170}}/> <TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}> <FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
<InputLabel>Distribuidor</InputLabel> <InputLabel>Distribuidor</InputLabel>
<Select value={filtroIdDistribuidor} label="Distribuidor" onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}> <Select value={filtroIdDistribuidor} label="Distribuidor" onChange={(e) => setFiltroIdDistribuidor(e.target.value as number | string)}>
<MenuItem value=""><em>Todos</em></MenuItem> <MenuItem value=""><em>Todos</em></MenuItem>
{distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)} {distribuidores.map(d => <MenuItem key={d.idDistribuidor} value={d.idDistribuidor}>{d.nombre}</MenuItem>)}
</Select> </Select>
</FormControl> </FormControl>
<FormControl size="small" sx={{minWidth: 200, flexGrow: 1}} disabled={loadingFiltersDropdown}> <FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
<InputLabel>Empresa (Saldo)</InputLabel> <InputLabel>Empresa (Saldo)</InputLabel>
<Select value={filtroIdEmpresa} label="Empresa (Saldo)" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}> <Select value={filtroIdEmpresa} label="Empresa (Saldo)" onChange={(e) => setFiltroIdEmpresa(e.target.value as number | string)}>
<MenuItem value=""><em>Todas</em></MenuItem> <MenuItem value=""><em>Todas</em></MenuItem>
{empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)} {empresas.map(e => <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>)}
</Select> </Select>
</FormControl> </FormControl>
<FormControl size="small" sx={{minWidth: 150, flexGrow: 1}}> <FormControl size="small" sx={{ minWidth: 150, flexGrow: 1 }}>
<InputLabel>Tipo Mov.</InputLabel> <InputLabel>Tipo Mov.</InputLabel>
<Select value={filtroTipoMov} label="Tipo Mov." onChange={(e) => setFiltroTipoMov(e.target.value as 'Recibido' | 'Realizado' | '')}> <Select value={filtroTipoMov} label="Tipo Mov." onChange={(e) => setFiltroTipoMov(e.target.value as 'Recibido' | 'Realizado' | '')}>
<MenuItem value=""><em>Todos</em></MenuItem> <MenuItem value=""><em>Todos</em></MenuItem>
<MenuItem value="Recibido">Recibido</MenuItem> <MenuItem value="Recibido">Recibido</MenuItem>
<MenuItem value="Realizado">Realizado</MenuItem> <MenuItem value="Realizado">Realizado</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Box> </Box>
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Pago</Button>)} {puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Pago</Button>)}
</Paper> </Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>} {error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} {pageApiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{pageApiErrorMessage}</Alert>}
{!loading && !error && puedeVer && ( {!loading && !error && puedeVer && (
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table size="small"> <Table size="small">
<TableHead><TableRow> <TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Distribuidor</TableCell><TableCell>Empresa (Saldo)</TableCell> <TableCell>Fecha</TableCell><TableCell>Distribuidor</TableCell><TableCell>Empresa (Saldo)</TableCell>
<TableCell>Tipo Mov.</TableCell><TableCell>Recibo N°</TableCell> <TableCell>Tipo Mov.</TableCell><TableCell>Recibo N°</TableCell>
<TableCell align="right">Monto</TableCell><TableCell>Tipo Pago</TableCell> <TableCell align="right">Monto</TableCell><TableCell>Tipo Pago</TableCell>
<TableCell>Detalle</TableCell> <TableCell>Detalle</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>} {(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead> </TableRow></TableHead>
<TableBody> <TableBody>
{displayData.length === 0 ? ( {displayData.length === 0 ? (
<TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 9 : 8} align="center">No se encontraron pagos.</TableCell></TableRow> <TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 9 : 8} align="center">No se encontraron pagos.</TableCell></TableRow>
) : ( ) : (
displayData.map((p) => ( displayData.map((p) => (
<TableRow key={p.idPago} hover> <TableRow key={p.idPago} hover>
<TableCell>{formatDate(p.fecha)}</TableCell><TableCell>{p.nombreDistribuidor}</TableCell> <TableCell>{formatDate(p.fecha)}</TableCell><TableCell>{p.nombreDistribuidor}</TableCell>
<TableCell>{p.nombreEmpresa}</TableCell> <TableCell>{p.nombreEmpresa}</TableCell>
<TableCell> <TableCell>
<Chip label={p.tipoMovimiento} color={p.tipoMovimiento === 'Recibido' ? 'success' : 'warning'} size="small"/> <Chip label={p.tipoMovimiento} color={p.tipoMovimiento === 'Recibido' ? 'success' : 'warning'} size="small" />
</TableCell> </TableCell>
<TableCell>{p.recibo}</TableCell> <TableCell>{p.recibo}</TableCell>
<TableCell align="right">${p.monto.toFixed(2)}</TableCell> <TableCell align="right">${p.monto.toFixed(2)}</TableCell>
<TableCell>{p.nombreTipoPago}</TableCell> <TableCell>{p.nombreTipoPago}</TableCell>
<TableCell><Tooltip title={p.detalle || ''}><Box sx={{maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{p.detalle || '-'}</Box></Tooltip></TableCell> <TableCell><Tooltip title={p.detalle || ''}><Box sx={{ maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.detalle || '-'}</Box></Tooltip></TableCell>
{(puedeModificar || puedeEliminar) && ( {(puedeModificar || puedeEliminar) && (
<TableCell align="right"> <TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton> <IconButton onClick={(e) => handleMenuOpen(e, p)} disabled={!puedeModificar && !puedeEliminar}><MoreVertIcon /></IconButton>
</TableCell> </TableCell>
)} )}
</TableRow> </TableRow>
)))} )))}
</TableBody> </TableBody>
</Table> </Table>
<TablePagination <TablePagination
rowsPerPageOptions={[25, 50, 100]} component="div" count={pagos.length} rowsPerPageOptions={[25, 50, 100]} component="div" count={pagos.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:" onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/> />
</TableContainer> </TableContainer>
)} )}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && selectedRow && ( {puedeModificar && selectedRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Modificar</MenuItem>)} <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
{puedeEliminar && selectedRow && ( {puedeEliminar && selectedRow && (
<MenuItem onClick={() => handleDelete(selectedRow.idPago)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)} <MenuItem onClick={() => handleDelete(selectedRow.idPago)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)}
</Menu> </Menu>
<PagoDistribuidorFormModal <PagoDistribuidorFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal} open={modalOpen}
initialData={editingPago} errorMessage={apiErrorMessage} onClose={handleCloseModal}
clearErrorMessage={() => setApiErrorMessage(null)} onSubmit={handleSubmitModal}
initialData={editingPago}
errorMessage={modalApiErrorMessage}
clearErrorMessage={clearModalApiErrorMessage}
/> />
</Box> </Box>
); );

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

@@ -8,6 +8,7 @@ import reportesService from '../../services/Reportes/reportesService';
import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto'; import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto';
import SeleccionaReporteDetalleDistribucionCanillas from './SeleccionaReporteDetalleDistribucionCanillas'; import SeleccionaReporteDetalleDistribucionCanillas from './SeleccionaReporteDetalleDistribucionCanillas';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios'; import axios from 'axios';
// Para el tipo del footer en DataGridSectionProps // Para el tipo del footer en DataGridSectionProps
@@ -81,9 +82,12 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
const [currentParams, setCurrentParams] = useState<{ const [currentParams, setCurrentParams] = useState<{
fecha: string; fecha: string;
idEmpresa: number; idEmpresa: number;
esAccionista: boolean;
nombreEmpresa?: string; nombreEmpresa?: string;
} | null>(null); } | null>(null);
const [pdfSoloTotales, setPdfSoloTotales] = useState(false); const [pdfSoloTotales, setPdfSoloTotales] = useState(false);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVerReporte = isSuperAdmin || tienePermiso("MC005");
const initialTotals: TotalesComunes = { totalCantSalida: 0, totalCantEntrada: 0, vendidos: 0, totalRendir: 0 }; const initialTotals: TotalesComunes = { totalCantSalida: 0, totalCantEntrada: 0, vendidos: 0, totalRendir: 0 };
const [totalesCanillas, setTotalesCanillas] = useState<TotalesComunes>(initialTotals); const [totalesCanillas, setTotalesCanillas] = useState<TotalesComunes>(initialTotals);
@@ -115,16 +119,29 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
const handleGenerarReporte = useCallback(async (params: { const handleGenerarReporte = useCallback(async (params: {
fecha: string; fecha: string;
idEmpresa: number; idEmpresa: number;
esAccionista: boolean;
}) => { }) => {
if (!puedeVerReporte) {
setError("No tiene permiso para generar este reporte.");
setLoading(false);
return;
}
setLoading(true); setLoading(true);
setError(null); setError(null);
setApiErrorParams(null); setApiErrorParams(null);
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
const empData = await empresaService.getEmpresaById(params.idEmpresa);
setCurrentParams({ ...params, nombreEmpresa: empData?.nombre });
setReportData(null);
// Resetear totales let nombreEmpresa = `Empresa ID ${params.idEmpresa}`;
if (params.idEmpresa !== 0) {
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
const empData = await empresaService.getEmpresaById(params.idEmpresa);
nombreEmpresa = empData?.nombre ?? nombreEmpresa;
} else {
nombreEmpresa = "TODAS";
}
setCurrentParams({ ...params, nombreEmpresa });
setReportData(null);
setTotalesCanillas(initialTotals); setTotalesCanillas(initialTotals);
setTotalesAccionistas(initialTotals); setTotalesAccionistas(initialTotals);
setTotalesCanillasOtraFecha(initialTotals); setTotalesCanillasOtraFecha(initialTotals);
@@ -140,7 +157,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
const processedData = { const processedData = {
canillas: addIds(data.canillas, 'can'), canillas: addIds(data.canillas, 'can'),
canillasAccionistas: addIds(data.canillasAccionistas, 'acc'), canillasAccionistas: addIds(data.canillasAccionistas, 'acc'),
canillasTodos: addIds(data.canillasTodos, 'all'), // Aún necesita IDs para DataGridSection canillasTodos: addIds(data.canillasTodos, 'all'),
canillasLiquidadasOtraFecha: addIds(data.canillasLiquidadasOtraFecha, 'canliq'), canillasLiquidadasOtraFecha: addIds(data.canillasLiquidadasOtraFecha, 'canliq'),
canillasAccionistasLiquidadasOtraFecha: addIds(data.canillasAccionistasLiquidadasOtraFecha, 'accliq'), canillasAccionistasLiquidadasOtraFecha: addIds(data.canillasAccionistasLiquidadasOtraFecha, 'accliq'),
controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'), controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'),
@@ -167,7 +184,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, []); }, [puedeVerReporte]);
const handleVolverAParametros = useCallback(() => { const handleVolverAParametros = useCallback(() => {
setShowParamSelector(true); setShowParamSelector(true);
@@ -188,10 +205,9 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
if (data && data.length > 0) { if (data && data.length > 0) {
const exportedData = data.map(item => { const exportedData = data.map(item => {
const row: Record<string, any> = {}; const row: Record<string, any> = {};
// Excluir el 'id' generado para DataGrid si existe
const { id, ...itemData } = item; const { id, ...itemData } = item;
Object.keys(fields).forEach(key => { Object.keys(fields).forEach(key => {
row[fields[key]] = (itemData as any)[key]; // Usar itemData row[fields[key]] = (itemData as any)[key];
if (key === 'fecha' && (itemData as any)[key]) { if (key === 'fecha' && (itemData as any)[key]) {
row[fields[key]] = new Date((itemData as any)[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' }); row[fields[key]] = new Date((itemData as any)[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' });
} }
@@ -215,18 +231,18 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
} }
}; };
// Definición de campos para la exportación
const fieldsCanillaAccionista = { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" }; const fieldsCanillaAccionista = { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha: "Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" }; const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha: "Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsTodos = { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" }; const fieldsTodos = { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
formatAndSheet(reportData.canillas, "Canillitas_Dia", fieldsCanillaAccionista); formatAndSheet(reportData.canillas, "Canillitas_Dia", fieldsCanillaAccionista);
formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", fieldsCanillaAccionista); formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", fieldsCanillaAccionista);
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos); if (currentParams?.idEmpresa !== 0) {
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos);
}
formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", fieldsCanillaAccionistaFechaLiq); formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", fieldsCanillaAccionistaFechaLiq);
formatAndSheet(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas_OtrasFechas", fieldsCanillaAccionistaFechaLiq); formatAndSheet(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas_OtrasFechas", fieldsCanillaAccionistaFechaLiq);
let fileName = "ReporteDetalleDistribucionCanillitas"; let fileName = "ReporteDetalleDistribucionCanillitas";
if (currentParams) { if (currentParams) {
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`; fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
@@ -265,8 +281,6 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
} }
}, [currentParams]); }, [currentParams]);
// --- Definiciones de Columnas ---
const commonColumns: GridColDef[] = [ const commonColumns: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 }, { field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.3 }, { field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.3 },
@@ -295,8 +309,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) }, { field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
]; ];
// --- Custom Footers --- const createCustomFooterComponent = (totals: TotalesComunes, columnsDef: GridColDef[]): CustomFooterType => {
const createCustomFooterComponent = (totals: TotalesComunes, columnsDef: GridColDef[]): CustomFooterType => { // Especificar el tipo de retorno
const getCellStyle = (colConfig: GridColDef | undefined, isPlaceholder: boolean = false) => { const getCellStyle = (colConfig: GridColDef | undefined, isPlaceholder: boolean = false) => {
if (!colConfig) return { width: 100, textAlign: 'right' as const, pr: isPlaceholder ? 0 : 1, fontWeight: 'bold' }; if (!colConfig) return { width: 100, textAlign: 'right' as const, pr: isPlaceholder ? 0 : 1, fontWeight: 'bold' };
const defaultWidth = colConfig.field === 'publicacion' ? 200 : (colConfig.field === 'canilla' || colConfig.field === 'tipoVendedor' ? 150 : 100); const defaultWidth = colConfig.field === 'publicacion' ? 200 : (colConfig.field === 'canilla' || colConfig.field === 'tipoVendedor' ? 150 : 100);
@@ -310,10 +323,9 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
}; };
}; };
// eslint-disable-next-line react/display-name const FooterComponent: CustomFooterType = (props) => (
const FooterComponent: CustomFooterType = (props) => ( // El componente debe aceptar props <GridFooterContainer {...props} sx={{
<GridFooterContainer {...props} sx={{ // Pasar props y combinar sx ...(props.sx as any),
...(props.sx as any), // Castear props.sx temporalmente si es necesario
justifyContent: 'space-between', alignItems: 'center', width: '100%', justifyContent: 'space-between', alignItems: 'center', width: '100%',
borderTop: (theme) => `1px solid ${theme.palette.divider}`, minHeight: '52px', borderTop: (theme) => `1px solid ${theme.palette.divider}`, minHeight: '52px',
}}> }}>
@@ -339,6 +351,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
return FooterComponent; return FooterComponent;
}; };
// Usar los componentes creados con useMemo
const FooterCanillas = useMemo(() => createCustomFooterComponent(totalesCanillas, commonColumns), [totalesCanillas]); const FooterCanillas = useMemo(() => createCustomFooterComponent(totalesCanillas, commonColumns), [totalesCanillas]);
const FooterAccionistas = useMemo(() => createCustomFooterComponent(totalesAccionistas, commonColumns), [totalesAccionistas]); const FooterAccionistas = useMemo(() => createCustomFooterComponent(totalesAccionistas, commonColumns), [totalesAccionistas]);
const FooterCanillasOtraFecha = useMemo(() => createCustomFooterComponent(totalesCanillasOtraFecha, commonColumnsWithFecha), [totalesCanillasOtraFecha]); const FooterCanillasOtraFecha = useMemo(() => createCustomFooterComponent(totalesCanillasOtraFecha, commonColumnsWithFecha), [totalesCanillasOtraFecha]);
@@ -346,12 +359,16 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
const FooterResumen = useMemo(() => createCustomFooterComponent(totalesResumen, columnsTodos), [totalesResumen, columnsTodos]); const FooterResumen = useMemo(() => createCustomFooterComponent(totalesResumen, columnsTodos), [totalesResumen, columnsTodos]);
if (showParamSelector) { if (showParamSelector) {
if (!loading && !puedeVerReporte) {
return <Alert severity="error" sx={{ m: 2 }}>No tiene permiso para acceder a este reporte.</Alert>;
}
return ( return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}> <Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}> <Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
<SeleccionaReporteDetalleDistribucionCanillas <SeleccionaReporteDetalleDistribucionCanillas
onGenerarReporte={handleGenerarReporte} onGenerarReporte={handleGenerarReporte}
onCancel={handleVolverAParametros} // Aunque el componente no lo use directamente. onCancel={handleVolverAParametros}
isLoading={loading} isLoading={loading}
apiErrorMessage={apiErrorParams} apiErrorMessage={apiErrorParams}
/> />
@@ -360,17 +377,45 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
); );
} }
const renderContent = () => {
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>;
if (error && !loading) return <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>;
if (!reportData) return <Typography sx={{ mt: 2, fontStyle: 'italic' }}>No se encontraron datos.</Typography>;
if (currentParams?.idEmpresa === 0) {
if (currentParams.esAccionista) {
return <DataGridSection title="Accionistas (Todas las Empresas)" data={reportData.canillasAccionistas || []} columns={commonColumns} footerComponent={FooterAccionistas} />;
}
return <DataGridSection title="Canillitas (Todas las Empresas)" data={reportData.canillas || []} columns={commonColumns} footerComponent={FooterCanillas} />;
}
return (
<>
<DataGridSection title="Canillitas" data={reportData.canillas || []} columns={commonColumns} footerComponent={FooterCanillas} />
<DataGridSection title="Accionistas" data={reportData.canillasAccionistas || []} columns={commonColumns} footerComponent={FooterAccionistas} />
<DataGridSection title="Resumen por Tipo de Vendedor" data={reportData.canillasTodos || []} columns={columnsTodos} footerComponent={FooterResumen} />
{reportData.canillasLiquidadasOtraFecha && reportData.canillasLiquidadasOtraFecha.length > 0 &&
<DataGridSection title="Canillitas (Liquidados de Otras Fechas)" data={reportData.canillasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterCanillasOtraFecha} />}
{reportData.canillasAccionistasLiquidadasOtraFecha && reportData.canillasAccionistasLiquidadasOtraFecha.length > 0 &&
<DataGridSection title="Accionistas (Liquidados de Otras Fechas)" data={reportData.canillasAccionistasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterAccionistasOtraFecha} />}
</>
);
};
return ( return (
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h5">Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', { timeZone: 'UTC' }) : ''}</Typography> <Typography variant="h5">Reporte: Detalle Distribución ({currentParams?.nombreEmpresa}) - {currentParams?.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', { timeZone: 'UTC' }) : ''}</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{currentParams?.idEmpresa !== 0 && (
<Button onClick={() => handleGenerarYAbrirPdf(true)} variant="contained" color="secondary" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf && pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Totales"}
</Button>
)}
<Button onClick={() => handleGenerarYAbrirPdf(false)} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small"> <Button onClick={() => handleGenerarYAbrirPdf(false)} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf && !pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Detalle"} {loadingPdf && !pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Detalle"}
</Button> </Button>
<Button onClick={() => handleGenerarYAbrirPdf(true)} variant="contained" color="secondary" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf && pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Totales"}
</Button>
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small"> <Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small">
Exportar a Excel Exportar a Excel
</Button> </Button>
@@ -379,34 +424,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
</Button> </Button>
</Box> </Box>
</Box> </Box>
{renderContent()}
{loading && <Box sx={{ textAlign: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData && (
<>
<DataGridSection title="Canillitas" data={reportData.canillas || []} columns={commonColumns} footerComponent={FooterCanillas} />
<DataGridSection title="Accionistas" data={reportData.canillasAccionistas || []} columns={commonColumns} footerComponent={FooterAccionistas} />
<DataGridSection
title="Resumen por Tipo de Vendedor"
data={reportData.canillasTodos || []}
columns={columnsTodos}
footerComponent={FooterResumen} // <-- PASAR EL FOOTER
height={220} // El height ya no es necesario si autoHeight está activado por tener footer
/>
{reportData.canillasLiquidadasOtraFecha && reportData.canillasLiquidadasOtraFecha.length > 0 &&
<DataGridSection title="Canillitas (Liquidados de Otras Fechas)" data={reportData.canillasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterCanillasOtraFecha} />}
{reportData.canillasAccionistasLiquidadasOtraFecha && reportData.canillasAccionistasLiquidadasOtraFecha.length > 0 &&
<DataGridSection title="Accionistas (Liquidados de Otras Fechas)" data={reportData.canillasAccionistasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterAccionistasOtraFecha} />}
</>
)}
{!loading && !error && reportData &&
Object.values(reportData).every(arr => !arr || arr.length === 0) &&
<Typography sx={{ mt: 2, fontStyle: 'italic' }}>No se encontraron datos para los criterios seleccionados.</Typography>
}
</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

@@ -3,92 +3,80 @@ import { Box, Paper, Typography, List, ListItemButton, ListItemText, Collapse, C
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import ExpandLess from '@mui/icons-material/ExpandLess'; import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore'; import ExpandMore from '@mui/icons-material/ExpandMore';
import { usePermissions } from '../../hooks/usePermissions';
// Definición de los módulos de reporte con sus categorías, etiquetas y rutas const allReportModules: { category: string; label: string; path: string; requiredPermission: string; }[] = [
const allReportModules: { category: string; label: string; path: string }[] = [ { category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel', requiredPermission: 'RR005' },
{ category: 'Existencia Papel', label: 'Existencia de Papel', path: 'existencia-papel' }, { category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas', requiredPermission: 'RR006' },
{ category: 'Movimientos Bobinas', label: 'Movimiento de Bobinas', path: 'movimiento-bobinas' }, { category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado', requiredPermission: 'RR006' },
{ category: 'Movimientos Bobinas', label: 'Mov. Bobinas por Estado', path: 'movimiento-bobinas-estado' }, { category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distribución Distribuidores', path: 'listado-distribucion-distribuidores' }, { category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distribución Canillas', path: 'listado-distribucion-canillas' }, { category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distribución General', path: 'listado-distribucion-general' }, { category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe', requiredPermission: 'RR002' },
{ category: 'Listados Distribución', label: 'Distrib. Canillas (Importe)', path: 'listado-distribucion-canillas-importe' }, { category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas', requiredPermission: 'MC005' },
{ category: 'Listados Distribución', label: 'Detalle Distribución Canillas', path: 'detalle-distribucion-canillas' }, { category: 'Secretaría', label: 'Reportes de Ventas', path: 'venta-mensual-secretaria', requiredPermission: 'RR012' },
{ category: 'Secretaría', label: 'Venta Mensual Secretaría', path: 'venta-mensual-secretaria' }, { category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones', requiredPermission: 'RR008' },
{ category: 'Tiradas por Publicación', label: 'Tiradas Publicación/Sección', path: 'tiradas-publicaciones-secciones' }, { category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion', requiredPermission: 'RR007' },
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/Sección', path: 'consumo-bobinas-seccion' }, { category: 'Consumos Bobinas', label: 'Consumo Bobinas/Publicación', path: 'consumo-bobinas-publicacion', requiredPermission: 'RR007' },
{ category: 'Consumos Bobinas', label: 'Consumo Bobinas/PubPublicación', path: 'consumo-bobinas-publicacion' }, { category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas', requiredPermission: 'RR007' },
{ category: 'Consumos Bobinas', label: 'Comparativa Consumo Bobinas', path: 'comparativa-consumo-bobinas' }, { category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores', requiredPermission: 'RR001' },
{ category: 'Balance de Cuentas', label: 'Cuentas Distribuidores', path: 'cuentas-distribuidores' }, { category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones', requiredPermission: 'RR003' },
{ category: 'Ctrl. Devoluciones', label: 'Control de Devoluciones', path: 'control-devoluciones' }, { category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas', requiredPermission: 'RR004' },
{ category: 'Novedades de Canillitas', label: 'Novedades de Canillitas', path: 'novedades-canillas' }, { category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual', requiredPermission: 'RR009' },
{ category: 'Listados Distribución', label: 'Dist. Mensual Can/Acc', path: 'listado-distribucion-mensual' }, { category: 'Suscripciones', label: 'Facturas para Publicidad', path: 'suscripciones-facturas-publicidad', requiredPermission: 'RR010' },
{ category: 'Suscripciones', label: 'Facturas para Publicidad', path: 'suscripciones-facturas-publicidad' }, { category: 'Suscripciones', label: 'Distribución de Suscripciones', path: 'suscripciones-distribucion', requiredPermission: 'RR011' },
{ category: 'Suscripciones', label: 'Distribución de Suscripciones', path: 'suscripciones-distribucion' },
]; ];
const predefinedCategoryOrder = [ const predefinedCategoryOrder = [
'Balance de Cuentas', 'Balance de Cuentas', 'Listados Distribución', 'Ctrl. Devoluciones',
'Listados Distribución', 'Novedades de Canillitas', 'Suscripciones', 'Existencia Papel',
'Ctrl. Devoluciones', 'Movimientos Bobinas', 'Consumos Bobinas', 'Tiradas por Publicación', 'Secretaría',
'Novedades de Canillitas',
'Suscripciones',
'Existencia Papel',
'Movimientos Bobinas',
'Consumos Bobinas',
'Tiradas por Publicación',
'Secretaría',
]; ];
const ReportesIndexPage: React.FC = () => { const ReportesIndexPage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [expandedCategory, setExpandedCategory] = useState<string | false>(false); const [expandedCategory, setExpandedCategory] = useState<string | false>(false);
const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true); const [isLoadingInitialNavigation, setIsLoadingInitialNavigation] = useState(true);
const { tienePermiso, isSuperAdmin } = usePermissions();
const uniqueCategories = useMemo(() => predefinedCategoryOrder, []); const accessibleReportModules = useMemo(() => {
return allReportModules.filter(module =>
isSuperAdmin || tienePermiso(module.requiredPermission)
);
}, [isSuperAdmin, tienePermiso]);
const accessibleCategories = useMemo(() => {
const categoriesWithAccess = new Set(accessibleReportModules.map(r => r.category));
return predefinedCategoryOrder.filter(category => categoriesWithAccess.has(category));
}, [accessibleReportModules]);
useEffect(() => { useEffect(() => {
const currentBasePath = '/reportes'; const currentBasePath = '/reportes';
const pathParts = location.pathname.substring(currentBasePath.length + 1).split('/'); const pathParts = location.pathname.substring(currentBasePath.length + 1).split('/');
const subPathSegment = pathParts[0]; const subPathSegment = pathParts[0];
let activeReportFoundInEffect = false; if (subPathSegment) {
const activeReport = accessibleReportModules.find(module => module.path === subPathSegment);
if (subPathSegment && subPathSegment !== "") { // Asegurarse que subPathSegment no esté vacío
const activeReport = allReportModules.find(module => module.path === subPathSegment);
if (activeReport) { if (activeReport) {
setExpandedCategory(activeReport.category); setExpandedCategory(activeReport.category);
activeReportFoundInEffect = true;
} else { } else {
// Si la URL apunta a un reporte que no es accesible, no expandimos nada
setExpandedCategory(false); setExpandedCategory(false);
} }
} else { } else {
setExpandedCategory(false); // Si estamos en la página base (/reportes), expandimos la primera categoría disponible.
if (accessibleCategories.length > 0) {
setExpandedCategory(accessibleCategories[0]);
} else {
setExpandedCategory(false);
}
} }
if (location.pathname === currentBasePath && allReportModules.length > 0 && isLoadingInitialNavigation) { // No hay navegación automática, solo manejamos el estado de carga.
let firstReportToNavigate: { category: string; label: string; path: string } | null = null; setIsLoadingInitialNavigation(false);
for (const category of uniqueCategories) {
const reportsInCat = allReportModules.filter(r => r.category === category);
if (reportsInCat.length > 0) {
firstReportToNavigate = reportsInCat[0];
break;
}
}
if (firstReportToNavigate) {
navigate(firstReportToNavigate.path, { replace: true });
activeReportFoundInEffect = true;
}
}
// Solo se establece a false si no estamos en el proceso de navegación inicial O si no se encontró reporte
if (!activeReportFoundInEffect || location.pathname !== currentBasePath) {
setIsLoadingInitialNavigation(false);
}
}, [location.pathname, navigate, uniqueCategories, isLoadingInitialNavigation]); }, [location.pathname, accessibleReportModules, accessibleCategories]);
const handleCategoryClick = (categoryName: string) => { const handleCategoryClick = (categoryName: string) => {
setExpandedCategory(prev => (prev === categoryName ? false : categoryName)); setExpandedCategory(prev => (prev === categoryName ? false : categoryName));
@@ -99,12 +87,10 @@ const ReportesIndexPage: React.FC = () => {
}; };
const isReportActive = (reportPath: string) => { const isReportActive = (reportPath: string) => {
return location.pathname === `/reportes/${reportPath}` || location.pathname.startsWith(`/reportes/${reportPath}/`); return location.pathname.startsWith(`/reportes/${reportPath}`);
}; };
// Si isLoadingInitialNavigation es true Y estamos en /reportes, mostrar loader if (isLoadingInitialNavigation) {
// Esto evita mostrar el loader si se navega directamente a un sub-reporte.
if (isLoadingInitialNavigation && (location.pathname === '/reportes' || location.pathname === '/reportes/')) {
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}> <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}>
<CircularProgress /> <CircularProgress />
@@ -113,44 +99,25 @@ const ReportesIndexPage: React.FC = () => {
} }
return ( return (
// Contenedor principal que se adaptará a su padre
// Eliminamos 'height: calc(100vh - 64px)' y cualquier margen/padding que controle el espacio exterior
<Box sx={{ display: 'flex', width: '100%', height: '100%' }}> <Box sx={{ display: 'flex', width: '100%', height: '100%' }}>
{/* Panel Lateral para Navegación */} <Paper elevation={0} square sx={{
<Paper width: { xs: 220, sm: 250, md: 280 },
elevation={0} // Sin elevación para que se sienta más integrado si el fondo es el mismo
square // Bordes rectos
sx={{
width: { xs: 220, sm: 250, md: 280 }, // Ancho responsivo del panel lateral
minWidth: { xs: 200, sm: 220 }, minWidth: { xs: 200, sm: 220 },
height: '100%', // Ocupa toda la altura del Box padre height: '100%',
borderRight: (theme) => `1px solid ${theme.palette.divider}`, borderRight: (theme) => `1px solid ${theme.palette.divider}`,
overflowY: 'auto', overflowY: 'auto',
bgcolor: 'background.paper', // O el color que desees para el menú bgcolor: 'background.paper',
// display: 'flex', flexDirection: 'column' // Para que el título y la lista usen el espacio vertical }}>
}} <Box sx={{ p: 1.5 }}>
> <Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 }}>
{/* Título del Menú Lateral */}
<Box
sx={{
p: 1.5, // Padding interno para el título
// borderBottom: (theme) => `1px solid ${theme.palette.divider}`, // Opcional: separador
// position: 'sticky', // Si quieres que el título quede fijo al hacer scroll en la lista
// top: 0,
// zIndex: 1,
// bgcolor: 'background.paper' // Necesario si es sticky y tiene scroll la lista
}}
>
<Typography variant="h6" component="div" sx={{ fontWeight: 'medium', ml:1 /* Pequeño margen para alinear con items */ }}>
Reportes Reportes
</Typography> </Typography>
</Box> </Box>
{/* Lista de Categorías y Reportes */} {accessibleCategories.length > 0 ? (
{uniqueCategories.length > 0 ? ( <List component="nav" dense sx={{ pt: 0 }}>
<List component="nav" dense sx={{ pt: 0 }} /* Quitar padding superior de la lista si el título ya lo tiene */ > {accessibleCategories.map((category) => {
{uniqueCategories.map((category) => { const reportsInCategory = accessibleReportModules.filter(r => r.category === category);
const reportsInCategory = allReportModules.filter(r => r.category === category);
const isExpanded = expandedCategory === category; const isExpanded = expandedCategory === category;
return ( return (
@@ -158,26 +125,14 @@ const ReportesIndexPage: React.FC = () => {
<ListItemButton <ListItemButton
onClick={() => handleCategoryClick(category)} onClick={() => handleCategoryClick(category)}
sx={{ sx={{
// py: 1.2, // Ajustar padding vertical de items de categoría
// backgroundColor: isExpanded ? 'action.selected' : 'transparent',
borderLeft: isExpanded ? (theme) => `4px solid ${theme.palette.primary.main}` : '4px solid transparent', borderLeft: isExpanded ? (theme) => `4px solid ${theme.palette.primary.main}` : '4px solid transparent',
pr: 1, // Menos padding a la derecha para dar espacio al ícono expander pr: 1,
'&:hover': { '&:hover': { backgroundColor: 'action.hover' }
backgroundColor: 'action.hover' }}>
} <ListItemText primary={category} primaryTypographyProps={{ fontWeight: isExpanded ? 'bold' : 'normal' }}/>
}} {isExpanded ? <ExpandLess /> : <ExpandMore />}
>
<ListItemText
primary={category}
primaryTypographyProps={{
fontWeight: isExpanded ? 'bold' : 'normal',
// color: isExpanded ? 'primary.main' : 'text.primary'
}}
/>
{reportsInCategory.length > 0 && (isExpanded ? <ExpandLess /> : <ExpandMore />)}
</ListItemButton> </ListItemButton>
{reportsInCategory.length > 0 && ( <Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<List component="div" disablePadding dense> <List component="div" disablePadding dense>
{reportsInCategory.map((report) => ( {reportsInCategory.map((report) => (
<ListItemButton <ListItemButton
@@ -185,62 +140,39 @@ const ReportesIndexPage: React.FC = () => {
selected={isReportActive(report.path)} selected={isReportActive(report.path)}
onClick={() => handleReportClick(report.path)} onClick={() => handleReportClick(report.path)}
sx={{ sx={{
pl: 3.5, // Indentación para los reportes (ajustar si se cambió el padding del título) pl: 3.5, py: 0.8,
py: 0.8, // Padding vertical de items de reporte
...(isReportActive(report.path) && { ...(isReportActive(report.path) && {
backgroundColor: (theme) => theme.palette.action.selected, // Un color de fondo sutil backgroundColor: (theme) => theme.palette.action.selected,
borderLeft: (theme) => `4px solid ${theme.palette.primary.light}`, // Un borde para el activo borderLeft: (theme) => `4px solid ${theme.palette.primary.light}`,
'& .MuiListItemText-primary': { '& .MuiListItemText-primary': { fontWeight: 'medium' },
fontWeight: 'medium', // O 'bold'
// color: 'primary.main'
},
}), }),
'&:hover': { '&:hover': { backgroundColor: (theme) => theme.palette.action.hover }
backgroundColor: (theme) => theme.palette.action.hover }}>
}
}}
>
<ListItemText primary={report.label} primaryTypographyProps={{ variant: 'body2' }}/> <ListItemText primary={report.label} primaryTypographyProps={{ variant: 'body2' }}/>
</ListItemButton> </ListItemButton>
))} ))}
</List> </List>
</Collapse> </Collapse>
)}
{reportsInCategory.length === 0 && isExpanded && (
<ListItemText
primary="No hay reportes en esta categoría."
sx={{ pl: 3.5, fontStyle: 'italic', color: 'text.secondary', py:1, typography: 'body2' }}
/>
)}
</React.Fragment> </React.Fragment>
); );
})} })}
</List> </List>
) : ( ) : (
<Typography sx={{p:2, fontStyle: 'italic'}}>No hay categorías configuradas.</Typography> <Typography sx={{p:2, fontStyle: 'italic'}}>No tiene acceso a ningún reporte.</Typography>
)} )}
</Paper> </Paper>
{/* Área Principal para el Contenido del Reporte */} <Box component="main" sx={{
<Box flexGrow: 1, p: { xs: 1, sm: 2, md: 3 },
component="main" overflowY: 'auto', height: '100%', bgcolor: 'grey.100'
sx={{ }}>
flexGrow: 1, // Ocupa el espacio restante {/* Lógica para mostrar el mensaje de bienvenida */}
p: { xs: 1, sm: 2, md: 3 }, // Padding interno para el contenido, responsivo {location.pathname === '/reportes' && !isLoadingInitialNavigation && (
overflowY: 'auto',
height: '100%', // Ocupa toda la altura del Box padre
bgcolor: 'grey.100' // Un color de fondo diferente para distinguir el área de contenido
}}
>
{/* El Outlet renderiza el componente del reporte específico */}
{(!location.pathname.startsWith('/reportes/') || !allReportModules.some(r => isReportActive(r.path))) && location.pathname !== '/reportes/' && location.pathname !== '/reportes' && !isLoadingInitialNavigation && (
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
El reporte solicitado no existe o la ruta no es válida.
</Typography>
)}
{(location.pathname === '/reportes/' || location.pathname === '/reportes') && !allReportModules.some(r => isReportActive(r.path)) && !isLoadingInitialNavigation && (
<Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}> <Typography sx={{p:2, textAlign:'center', mt: 4, color: 'text.secondary'}}>
{allReportModules.length > 0 ? "Seleccione una categoría y un reporte del menú lateral." : "No hay reportes configurados."} {accessibleReportModules.length > 0
? "Seleccione una categoría y un reporte del menú lateral."
: "No tiene acceso a ningún reporte."
}
</Typography> </Typography>
)} )}
<Outlet /> <Outlet />

View File

@@ -1,16 +1,16 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
Box, Typography, TextField, Button, CircularProgress, Alert, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem FormControl, InputLabel, Select, MenuItem, ToggleButtonGroup, ToggleButton
} from '@mui/material'; } from '@mui/material';
import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto';
import empresaService from '../../services/Distribucion/empresaService'; import empresaService from '../../services/Distribucion/empresaService';
interface SeleccionaReporteDetalleDistribucionCanillasProps { interface SeleccionaReporteDetalleDistribucionCanillasProps {
onGenerarReporte: (params: { onGenerarReporte: (params: {
fecha: string; fecha: string;
idEmpresa: number; idEmpresa: number;
// soloTotales: boolean; // Podríamos añadirlo si el usuario elige la versión del PDF esAccionista: boolean; // Añadimos este parámetro
}) => Promise<void>; }) => Promise<void>;
onCancel: () => void; onCancel: () => void;
isLoading?: boolean; isLoading?: boolean;
@@ -24,9 +24,9 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC<SeleccionaReporteDe
}) => { }) => {
const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]); const [fecha, setFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [idEmpresa, setIdEmpresa] = useState<number | string>(''); const [idEmpresa, setIdEmpresa] = useState<number | string>('');
// const [soloTotales, setSoloTotales] = useState<boolean>(false); // Si se añade la opción const [esAccionista, setEsAccionista] = useState<boolean>(false); // Nuevo estado
const [empresas, setEmpresas] = useState<EmpresaDto[]>([]); const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]);
const [loadingDropdowns, setLoadingDropdowns] = useState(false); const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
@@ -34,8 +34,9 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC<SeleccionaReporteDe
const fetchEmpresas = async () => { const fetchEmpresas = async () => {
setLoadingDropdowns(true); setLoadingDropdowns(true);
try { try {
const data = await empresaService.getAllEmpresas(); // Asume que este servicio existe const data = await empresaService.getEmpresasDropdown();
setEmpresas(data); // Añadimos la opción "TODAS" al principio
setEmpresas([{ idEmpresa: 0, nombre: 'TODAS' }, ...data]);
} catch (error) { } catch (error) {
console.error("Error al cargar empresas:", error); console.error("Error al cargar empresas:", error);
setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar empresas.' })); setLocalErrors(prev => ({ ...prev, dropdowns: 'Error al cargar empresas.' }));
@@ -49,7 +50,8 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC<SeleccionaReporteDe
const validate = (): boolean => { const validate = (): boolean => {
const errors: { [key: string]: string | null } = {}; const errors: { [key: string]: string | null } = {};
if (!fecha) errors.fecha = 'La fecha es obligatoria.'; if (!fecha) errors.fecha = 'La fecha es obligatoria.';
if (!idEmpresa) errors.idEmpresa = 'Debe seleccionar una empresa.'; // El idEmpresa ya no puede estar vacío, porque se preselecciona "TODAS" o una empresa
if (idEmpresa === '') errors.idEmpresa = 'Debe seleccionar una empresa.';
setLocalErrors(errors); setLocalErrors(errors);
return Object.keys(errors).length === 0; return Object.keys(errors).length === 0;
}; };
@@ -59,14 +61,14 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC<SeleccionaReporteDe
onGenerarReporte({ onGenerarReporte({
fecha, fecha,
idEmpresa: Number(idEmpresa), idEmpresa: Number(idEmpresa),
// soloTotales // Si se añade la opción esAccionista // Pasamos el nuevo parámetro
}); });
}; };
return ( return (
<Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 380 }}> <Box sx={{ p: 2, border: '1px solid #ccc', borderRadius: '4px', minWidth: 420 }}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Parámetros: Detalle Distribución Canillitas Parámetros: Detalle Distribución Canillas
</Typography> </Typography>
<TextField <TextField
label="Fecha" label="Fecha"
@@ -89,26 +91,32 @@ const SeleccionaReporteDetalleDistribucionCanillas: React.FC<SeleccionaReporteDe
value={idEmpresa} value={idEmpresa}
onChange={(e) => { setIdEmpresa(e.target.value as number); setLocalErrors(p => ({ ...p, idEmpresa: null })); }} onChange={(e) => { setIdEmpresa(e.target.value as number); setLocalErrors(p => ({ ...p, idEmpresa: null })); }}
> >
<MenuItem value="" disabled><em>Seleccione una empresa</em></MenuItem>
{empresas.map((e) => ( {empresas.map((e) => (
<MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem> <MenuItem key={e.idEmpresa} value={e.idEmpresa}>{e.nombre}</MenuItem>
))} ))}
</Select> </Select>
{localErrors.idEmpresa && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idEmpresa}</Typography>} {localErrors.idEmpresa && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idEmpresa}</Typography>}
</FormControl> </FormControl>
{/*
<FormControlLabel {/* Selector condicional para Canillitas/Accionistas */}
control={ {idEmpresa === 0 && (
<Checkbox <Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
checked={soloTotales} <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'medium' }}>
onChange={(e) => setSoloTotales(e.target.checked)} Mostrar para todas las empresas
</Typography>
<ToggleButtonGroup
value={esAccionista ? 'accionistas' : 'canillitas'}
exclusive
onChange={(_, value) => { if (value !== null) setEsAccionista(value === 'accionistas'); }}
aria-label="Tipo de Vendedor"
disabled={isLoading} disabled={isLoading}
/> color="primary"
} >
label="Generar solo resumen de totales (PDF)" <ToggleButton value="canillitas">Canillitas</ToggleButton>
sx={{ mt: 1, mb: 1 }} <ToggleButton value="accionistas">Accionistas</ToggleButton>
/> </ToggleButtonGroup>
*/} </Box>
)}
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>} {apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>} {localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}

View File

@@ -3,7 +3,7 @@ import {
Box, Typography, TextField, Button, CircularProgress, Alert, Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem FormControl, InputLabel, Select, MenuItem
} from '@mui/material'; } from '@mui/material';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
import publicacionService from '../../services/Distribucion/publicacionService'; import publicacionService from '../../services/Distribucion/publicacionService';
interface SeleccionaReporteListadoDistribucionCanillasProps { interface SeleccionaReporteListadoDistribucionCanillasProps {
@@ -26,7 +26,7 @@ const SeleccionaReporteListadoDistribucionCanillas: React.FC<SeleccionaReporteLi
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
const [loadingDropdowns, setLoadingDropdowns] = useState(false); const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({}); const [localErrors, setLocalErrors] = useState<{ [key: string]: string | null }>({});
@@ -34,7 +34,7 @@ const SeleccionaReporteListadoDistribucionCanillas: React.FC<SeleccionaReporteLi
const fetchPublicaciones = async () => { const fetchPublicaciones = async () => {
setLoadingDropdowns(true); setLoadingDropdowns(true);
try { try {
const data = await publicacionService.getAllPublicaciones(undefined, undefined); const data = await publicacionService.getPublicacionesForDropdown(undefined);
setPublicaciones(data.map(p => p)); setPublicaciones(data.map(p => p));
} catch (error) { } catch (error) {
console.error("Error al cargar publicaciones:", error); console.error("Error al cargar publicaciones:", error);

View File

@@ -24,8 +24,9 @@ const meses = [
{ value: 10, label: 'Octubre' }, { value: 11, label: 'Noviembre' }, { value: 12, label: 'Diciembre' } { value: 10, label: 'Octubre' }, { value: 11, label: 'Noviembre' }, { value: 12, label: 'Diciembre' }
]; ];
const estadosPago = ['Pendiente', 'Pagada', 'Rechazada', 'Anulada']; const estadosPago = ['Pendiente', 'Pagada', 'Pagada Parcialmente', 'Rechazada', 'Anulada'];
const estadosFacturacion = ['Pendiente de Facturar', 'Facturado']; const estadosFacturacion = ['Pendiente de Facturar', 'Facturado'];
const tiposFactura = ['Mensual', 'Alta'];
const SuscriptorRow: React.FC<{ const SuscriptorRow: React.FC<{
resumen: ResumenCuentaSuscriptorDto; resumen: ResumenCuentaSuscriptorDto;
@@ -33,50 +34,60 @@ const SuscriptorRow: React.FC<{
handleOpenHistorial: (factura: FacturaConsolidadaDto) => void; handleOpenHistorial: (factura: FacturaConsolidadaDto) => void;
}> = ({ resumen, handleMenuOpen, handleOpenHistorial }) => { }> = ({ resumen, handleMenuOpen, handleOpenHistorial }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const formatCurrency = (value: number) => `$${value.toFixed(2)}`;
return ( return (
<React.Fragment> <React.Fragment>
<TableRow sx={{ '& > *': { borderBottom: 'unset' } }} hover> <TableRow sx={{ '& > *': { borderBottom: 'unset' } }} hover>
<TableCell><IconButton size="small" onClick={() => setOpen(!open)}>{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}</IconButton></TableCell> <TableCell><IconButton size="small" onClick={() => setOpen(!open)}>{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}</IconButton></TableCell>
<TableCell component="th" scope="row">{resumen.nombreSuscriptor}</TableCell> <TableCell component="th" scope="row">{resumen.nombreSuscriptor}</TableCell>
<TableCell align="right"> <TableCell align="right">
<Typography variant="body2" sx={{ fontWeight: 'bold', color: resumen.saldoPendienteTotal > 0 ? 'error.main' : 'success.main' }}>${resumen.saldoPendienteTotal.toFixed(2)}</Typography> <Typography variant="body2" sx={{ fontWeight: 'bold', color: resumen.saldoPendienteTotal > 0 ? 'error.main' : 'success.main' }}>{formatCurrency(resumen.saldoPendienteTotal)}</Typography>
<Typography variant="caption" color="text.secondary">de ${resumen.importeTotal.toFixed(2)}</Typography> <Typography variant="caption" color="text.secondary">de {formatCurrency(resumen.importeTotal)}</Typography>
</TableCell> </TableCell>
<TableCell colSpan={5}></TableCell> <TableCell colSpan={6}></TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={8}> <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={9}> {/* <-- Ajustado para la nueva columna */}
<Collapse in={open} timeout="auto" unmountOnExit> <Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ margin: 1, padding: 2, backgroundColor: 'grey.50', borderRadius: 1 }}> <Box sx={{ margin: 1, padding: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
<Typography variant="h6" gutterBottom component="div" sx={{ fontSize: '1rem', fontWeight: 'bold' }}>Facturas del Período para {resumen.nombreSuscriptor}</Typography> <Typography variant="h6" gutterBottom component="div" sx={{ fontSize: '1rem', fontWeight: 'bold' }}>Facturas del Período para {resumen.nombreSuscriptor}</Typography>
<Table size="small"> <Table size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>Empresa</TableCell><TableCell align="right">Importe</TableCell> <TableCell>Empresa</TableCell>
<TableCell>Estado Pago</TableCell><TableCell>Estado Facturación</TableCell> <TableCell align="right">Importe Total</TableCell>
<TableCell>Nro. Factura</TableCell><TableCell align="right">Acciones</TableCell> <TableCell align="right">Pagado</TableCell>
<TableCell align="right">Saldo</TableCell>
<TableCell>Tipo Factura</TableCell>
<TableCell>Estado Pago</TableCell>
<TableCell>Estado Facturación</TableCell>
<TableCell>Nro. Factura</TableCell>
<TableCell align="right">Acciones</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{resumen.facturas.map((factura) => ( {resumen.facturas.map((factura) => {
<TableRow key={factura.idFactura}> const saldo = factura.importeFinal - factura.totalPagado;
<TableCell sx={{ fontWeight: 'medium' }}>{factura.nombreEmpresa}</TableCell> return (
<TableCell align="right">${factura.importeFinal.toFixed(2)}</TableCell> <TableRow key={factura.idFactura}>
<TableCell><Chip label={factura.estadoPago} size="small" color={factura.estadoPago === 'Pagada' ? 'success' : (factura.estadoPago === 'Rechazada' ? 'error' : 'default')} /></TableCell> <TableCell sx={{ fontWeight: 'medium' }}>{factura.nombreEmpresa}</TableCell>
<TableCell><Chip label={factura.estadoFacturacion} size="small" color={factura.estadoFacturacion === 'Facturado' ? 'info' : 'warning'} /></TableCell> <TableCell align="right">{formatCurrency(factura.importeFinal)}</TableCell>
<TableCell>{factura.numeroFactura || '-'}</TableCell> <TableCell align="right" sx={{ color: 'success.dark' }}>{formatCurrency(factura.totalPagado)}</TableCell>
<TableCell align="right"> <TableCell align="right" sx={{ fontWeight: 'bold', color: saldo > 0 ? 'error.main' : 'inherit' }}>{formatCurrency(saldo)}</TableCell>
<IconButton onClick={(e) => handleMenuOpen(e, factura)} disabled={factura.estadoPago === 'Anulada'}> <TableCell>
<MoreVertIcon /> <Chip label={factura.tipoFactura} size="small" color={factura.tipoFactura === 'Alta' ? 'secondary' : 'default'} />
</IconButton> </TableCell>
<Tooltip title="Ver Historial de Envíos"> <TableCell><Chip label={factura.estadoPago} size="small" color={factura.estadoPago === 'Pagada' ? 'success' : (factura.estadoPago === 'Pagada Parcialmente' ? 'primary' : (factura.estadoPago === 'Rechazada' ? 'error' : 'default'))} /></TableCell>
<IconButton onClick={() => handleOpenHistorial(factura)}> <TableCell><Chip label={factura.estadoFacturacion} size="small" color={factura.estadoFacturacion === 'Facturado' ? 'info' : 'warning'} /></TableCell>
<MailOutlineIcon /> <TableCell>{factura.numeroFactura || '-'}</TableCell>
</IconButton> <TableCell align="right">
</Tooltip> <IconButton onClick={(e) => handleMenuOpen(e, factura)} disabled={factura.estadoPago === 'Anulada'}><MoreVertIcon /></IconButton>
</TableCell> <Tooltip title="Ver Historial de Envíos"><IconButton onClick={() => handleOpenHistorial(factura)}><MailOutlineIcon /></IconButton></Tooltip>
</TableRow> </TableCell>
))} </TableRow>
);
})}
</TableBody> </TableBody>
</Table> </Table>
</Box> </Box>
@@ -102,6 +113,7 @@ const ConsultaFacturasPage: React.FC = () => {
const [filtroNombre, setFiltroNombre] = useState(''); const [filtroNombre, setFiltroNombre] = useState('');
const [filtroEstadoPago, setFiltroEstadoPago] = useState(''); const [filtroEstadoPago, setFiltroEstadoPago] = useState('');
const [filtroEstadoFacturacion, setFiltroEstadoFacturacion] = useState(''); const [filtroEstadoFacturacion, setFiltroEstadoFacturacion] = useState('');
const [filtroTipoFactura, setFiltroTipoFactura] = useState('');
const [selectedFactura, setSelectedFactura] = useState<FacturaConsolidadaDto | null>(null); const [selectedFactura, setSelectedFactura] = useState<FacturaConsolidadaDto | null>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
@@ -121,7 +133,8 @@ const ConsultaFacturasPage: React.FC = () => {
selectedMes, selectedMes,
filtroNombre || undefined, filtroNombre || undefined,
filtroEstadoPago || undefined, filtroEstadoPago || undefined,
filtroEstadoFacturacion || undefined filtroEstadoFacturacion || undefined,
filtroTipoFactura || undefined
); );
setResumenes(data); setResumenes(data);
} catch (err) { } catch (err) {
@@ -130,7 +143,7 @@ const ConsultaFacturasPage: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [selectedAnio, selectedMes, puedeConsultar, filtroNombre, filtroEstadoPago, filtroEstadoFacturacion]); }, [selectedAnio, selectedMes, puedeConsultar, filtroNombre, filtroEstadoPago, filtroEstadoFacturacion, filtroTipoFactura]);
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@@ -162,7 +175,7 @@ const ConsultaFacturasPage: React.FC = () => {
setLoadingLogs(false); setLoadingLogs(false);
} }
}; };
const handleSubmitPagoModal = async (data: CreatePagoDto) => { const handleSubmitPagoModal = async (data: CreatePagoDto) => {
setApiError(null); setApiError(null);
try { try {
@@ -218,6 +231,17 @@ const ConsultaFacturasPage: React.FC = () => {
<TextField label="Buscar por Suscriptor" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} /> <TextField label="Buscar por Suscriptor" size="small" value={filtroNombre} onChange={(e) => setFiltroNombre(e.target.value)} sx={{ flexGrow: 1, minWidth: '200px' }} />
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Pago</InputLabel><Select value={filtroEstadoPago} label="Estado de Pago" onChange={(e) => setFiltroEstadoPago(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosPago.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl> <FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Pago</InputLabel><Select value={filtroEstadoPago} label="Estado de Pago" onChange={(e) => setFiltroEstadoPago(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosPago.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
<FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Facturación</InputLabel><Select value={filtroEstadoFacturacion} label="Estado de Facturación" onChange={(e) => setFiltroEstadoFacturacion(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosFacturacion.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl> <FormControl sx={{ minWidth: 200 }} size="small"><InputLabel>Estado de Facturación</InputLabel><Select value={filtroEstadoFacturacion} label="Estado de Facturación" onChange={(e) => setFiltroEstadoFacturacion(e.target.value)}><MenuItem value=""><em>Todos</em></MenuItem>{estadosFacturacion.map(e => <MenuItem key={e} value={e}>{e}</MenuItem>)}</Select></FormControl>
<FormControl sx={{ minWidth: 200 }} size="small">
<InputLabel>Tipo de Factura</InputLabel>
<Select
value={filtroTipoFactura}
label="Tipo de Factura"
onChange={(e) => setFiltroTipoFactura(e.target.value)}
>
<MenuItem value=""><em>Todos</em></MenuItem>
{tiposFactura.map(t => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</Select>
</FormControl>
</Box> </Box>
</Paper> </Paper>
@@ -226,15 +250,15 @@ const ConsultaFacturasPage: React.FC = () => {
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table aria-label="collapsible table"> <Table aria-label="collapsible table">
<TableHead><TableRow><TableCell /><TableCell>Suscriptor</TableCell><TableCell align="right">Saldo Total / Importe Total</TableCell><TableCell colSpan={5}></TableCell></TableRow></TableHead> <TableHead><TableRow><TableCell /><TableCell>Suscriptor</TableCell><TableCell align="right">Saldo Total / Importe Total</TableCell><TableCell colSpan={6}></TableCell></TableRow></TableHead>
<TableBody> <TableBody>
{loading ? (<TableRow><TableCell colSpan={8} align="center"><CircularProgress /></TableCell></TableRow>) {loading ? (<TableRow><TableCell colSpan={8} align="center"><CircularProgress /></TableCell></TableRow>)
: resumenes.length === 0 ? (<TableRow><TableCell colSpan={8} align="center">No hay facturas para el período seleccionado.</TableCell></TableRow>) : resumenes.length === 0 ? (<TableRow><TableCell colSpan={8} align="center">No hay facturas para el período seleccionado.</TableCell></TableRow>)
: (resumenes.map(resumen => ( : (resumenes.map(resumen => (
<SuscriptorRow <SuscriptorRow
key={resumen.idSuscriptor} key={resumen.idSuscriptor}
resumen={resumen} resumen={resumen}
handleMenuOpen={handleMenuOpen} handleMenuOpen={handleMenuOpen}
handleOpenHistorial={handleOpenHistorial} handleOpenHistorial={handleOpenHistorial}
/> />
)))} )))}
@@ -252,27 +276,14 @@ const ConsultaFacturasPage: React.FC = () => {
open={pagoModalOpen} open={pagoModalOpen}
onClose={handleClosePagoModal} onClose={handleClosePagoModal}
onSubmit={handleSubmitPagoModal} onSubmit={handleSubmitPagoModal}
factura={ factura={selectedFactura}
selectedFactura ? { nombreSuscriptor={
idFactura: selectedFactura.idFactura, resumenes.find(r => r.idSuscriptor === selectedFactura?.idSuscriptor)?.nombreSuscriptor || ''
nombreSuscriptor: resumenes.find(r => r.idSuscriptor === resumenes.find(res => res.facturas.some(f => f.idFactura === selectedFactura.idFactura))?.idSuscriptor)?.nombreSuscriptor || '',
importeFinal: selectedFactura.importeFinal,
saldoPendiente: selectedFactura.estadoPago === 'Pagada' ? 0 : selectedFactura.importeFinal,
idSuscriptor: resumenes.find(res => res.facturas.some(f => f.idFactura === selectedFactura.idFactura))?.idSuscriptor || 0,
periodo: '',
fechaEmision: '',
fechaVencimiento: '',
totalPagado: selectedFactura.importeFinal - (selectedFactura.estadoPago === 'Pagada' ? 0 : selectedFactura.importeFinal),
estadoPago: selectedFactura.estadoPago,
estadoFacturacion: selectedFactura.estadoFacturacion,
numeroFactura: selectedFactura.numeroFactura,
detalles: selectedFactura.detalles,
} : null
} }
errorMessage={apiError} errorMessage={apiError}
clearErrorMessage={() => setApiError(null)} clearErrorMessage={() => setApiError(null)}
/> />
<HistorialEnviosModal <HistorialEnviosModal
open={historialModalOpen} open={historialModalOpen}
onClose={() => setHistorialModalOpen(false)} onClose={() => setHistorialModalOpen(false)}

View File

@@ -15,12 +15,13 @@ import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklis
const SECCION_PERMISSIONS_PREFIX = "SS"; const SECCION_PERMISSIONS_PREFIX = "SS";
const getModuloFromSeccionCodAcc = (codAcc: string): string | null => { const getModuloFromSeccionCodAcc = (codAcc: string): string | null => {
if (codAcc === "SS001") return "Distribución"; if (codAcc === "SS001") return "Distribución";
if (codAcc === "SS007") return "Suscripciones";
if (codAcc === "SS002") return "Contables"; if (codAcc === "SS002") return "Contables";
if (codAcc === "SS003") return "Impresión"; if (codAcc === "SS003") return "Impresión";
if (codAcc === "SS004") return "Reportes"; if (codAcc === "SS004") return "Reportes";
if (codAcc === "SS006") return "Usuarios";
if (codAcc === "SS005") return "Radios"; if (codAcc === "SS005") return "Radios";
if (codAcc === "SS006") return "Usuarios";
return null; return null;
}; };
@@ -37,6 +38,9 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
moduloLower.includes("movimientos canillas") || moduloLower.includes("movimientos canillas") ||
moduloLower.includes("salidas otros destinos")) { moduloLower.includes("salidas otros destinos")) {
return "Distribución"; return "Distribución";
}
if (moduloLower.includes("suscripciones")) {
return "Suscripciones";
} }
if (moduloLower.includes("cuentas pagos") || if (moduloLower.includes("cuentas pagos") ||
moduloLower.includes("cuentas notas") || moduloLower.includes("cuentas notas") ||
@@ -50,9 +54,6 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
moduloLower.includes("tipos bobinas")) { moduloLower.includes("tipos bobinas")) {
return "Impresión"; return "Impresión";
} }
if (moduloLower.includes("radios")) {
return "Radios";
}
if (moduloLower.includes("usuarios") || if (moduloLower.includes("usuarios") ||
moduloLower.includes("perfiles")) { moduloLower.includes("perfiles")) {
return "Usuarios"; return "Usuarios";
@@ -62,6 +63,9 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => {
} }
if (moduloLower.includes("permisos")) { if (moduloLower.includes("permisos")) {
return "Permisos (Definición)"; return "Permisos (Definición)";
}
if (moduloLower.includes("radios")) {
return "Radios";
} }
return permisoModulo; return permisoModulo;
}; };

View File

@@ -1,11 +1,9 @@
// src/routes/AppRoutes.tsx
import React, { type JSX } from 'react'; import React, { type JSX } from 'react';
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom';
import LoginPage from '../pages/LoginPage'; import LoginPage from '../pages/LoginPage';
import HomePage from '../pages/HomePage'; import HomePage from '../pages/HomePage';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import MainLayout from '../layouts/MainLayout'; import MainLayout from '../layouts/MainLayout';
import { Typography } from '@mui/material';
import SectionProtectedRoute from './SectionProtectedRoute'; import SectionProtectedRoute from './SectionProtectedRoute';
// Distribución // Distribución
@@ -267,7 +265,6 @@ const AppRoutes = () => {
<ReportesIndexPage /> <ReportesIndexPage />
</SectionProtectedRoute>} </SectionProtectedRoute>}
> >
<Route index element={<Typography sx={{ p: 2 }}>Seleccione un reporte del menú lateral.</Typography>} /> {/* Placeholder */}
<Route path="existencia-papel" element={<ReporteExistenciaPapelPage />} /> <Route path="existencia-papel" element={<ReporteExistenciaPapelPage />} />
<Route path="movimiento-bobinas" element={<ReporteMovimientoBobinasPage />} /> <Route path="movimiento-bobinas" element={<ReporteMovimientoBobinasPage />} />
<Route path="movimiento-bobinas-estado" element={<ReporteMovimientoBobinasEstadoPage />} /> <Route path="movimiento-bobinas-estado" element={<ReporteMovimientoBobinasEstadoPage />} />

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

@@ -20,6 +20,7 @@ import type { NovedadesCanillasReporteDto } from '../../models/dtos/Reportes/Nov
import type { CanillaGananciaReporteDto } from '../../models/dtos/Reportes/CanillaGananciaReporteDto'; import type { CanillaGananciaReporteDto } from '../../models/dtos/Reportes/CanillaGananciaReporteDto';
import type { ListadoDistCanMensualDiariosDto } from '../../models/dtos/Reportes/ListadoDistCanMensualDiariosDto'; import type { ListadoDistCanMensualDiariosDto } from '../../models/dtos/Reportes/ListadoDistCanMensualDiariosDto';
import type { ListadoDistCanMensualPubDto } from '../../models/dtos/Reportes/ListadoDistCanMensualPubDto'; import type { ListadoDistCanMensualPubDto } from '../../models/dtos/Reportes/ListadoDistCanMensualPubDto';
import axios from 'axios';
interface GetExistenciaPapelParams { interface GetExistenciaPapelParams {
fechaDesde: string; // yyyy-MM-dd fechaDesde: string; // yyyy-MM-dd
@@ -209,24 +210,43 @@ const getVentaMensualSecretariaTirDevoPdf = async (params: { fechaDesde: string;
return response.data; return response.data;
}; };
const getReporteDistribucionCanillas = async (params: { const getReporteDistribucionCanillas = async (params: {
fecha: string; fecha: string;
idEmpresa: number; idEmpresa: number;
esAccionista?: boolean; // Hacerlo opcional
}): Promise<ReporteDistribucionCanillasResponseDto> => { }): Promise<ReporteDistribucionCanillasResponseDto> => {
const response = await apiClient.get<ReporteDistribucionCanillasResponseDto>('/reportes/distribucion-canillas', { params }); try {
return response.data; const response = await apiClient.get<ReporteDistribucionCanillasResponseDto>('/reportes/distribucion-canillas', { params });
return response.data;
} catch (error) {
console.error('Error al obtener datos del reporte de distribución de canillas:', error);
throw error;
}
}; };
const getReporteDistribucionCanillasPdf = async (params: { const getReporteDistribucionCanillasPdf = async (params: {
fecha: string; fecha: string;
idEmpresa: number; idEmpresa: number;
soloTotales: boolean; // Nuevo parámetro esAccionista?: boolean; // Opcional
soloTotales: boolean;
}): Promise<Blob> => { }): Promise<Blob> => {
const response = await apiClient.get('/reportes/distribucion-canillas/pdf', { // La ruta no necesita cambiar si el backend lo maneja try {
params, // soloTotales se enviará como query param si el backend lo espera así const response = await apiClient.get('/reportes/distribucion-canillas/pdf', {
responseType: 'blob', params,
}); responseType: 'blob'
return response.data; });
return response.data;
} catch (error) {
console.error('Error al generar PDF del reporte de distribución de canillas:', error);
if (axios.isAxiosError(error) && error.response?.data) {
// Si el error es un JSON dentro de un Blob, hay que leerlo
if (error.response.data.type === 'application/json') {
const errorJson = JSON.parse(await error.response.data.text());
throw new Error(errorJson.message || "Error al generar PDF");
}
}
throw error;
}
}; };
const getTiradasPublicacionesSecciones = async (params: { const getTiradasPublicacionesSecciones = async (params: {

View File

@@ -19,11 +19,16 @@ const procesarArchivoRespuesta = async (archivo: File): Promise<ProcesamientoLot
return response.data; return response.data;
}; };
const getResumenesDeCuentaPorPeriodo = async (anio: number, mes: number, nombreSuscriptor?: string, estadoPago?: string, estadoFacturacion?: string): Promise<ResumenCuentaSuscriptorDto[]> => { const getResumenesDeCuentaPorPeriodo = async (
anio: number, mes: number,
nombreSuscriptor?: string, estadoPago?: string, estadoFacturacion?: string,
tipoFactura?: string
): Promise<ResumenCuentaSuscriptorDto[]> => {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (nombreSuscriptor) params.append('nombreSuscriptor', nombreSuscriptor); if (nombreSuscriptor) params.append('nombreSuscriptor', nombreSuscriptor);
if (estadoPago) params.append('estadoPago', estadoPago); if (estadoPago) params.append('estadoPago', estadoPago);
if (estadoFacturacion) params.append('estadoFacturacion', estadoFacturacion); if (estadoFacturacion) params.append('estadoFacturacion', estadoFacturacion);
if (tipoFactura) params.append('tipoFactura', tipoFactura);
const queryString = params.toString(); const queryString = params.toString();
const url = `${API_URL}/${anio}/${mes}${queryString ? `?${queryString}` : ''}`; const url = `${API_URL}/${anio}/${mes}${queryString ? `?${queryString}` : ''}`;
@@ -81,10 +86,10 @@ const getHistorialLotesEnvio = async (anio?: number, mes?: number): Promise<Lote
const params = new URLSearchParams(); const params = new URLSearchParams();
if (anio) params.append('anio', String(anio)); if (anio) params.append('anio', String(anio));
if (mes) params.append('mes', String(mes)); if (mes) params.append('mes', String(mes));
const queryString = params.toString(); const queryString = params.toString();
const url = `${API_URL}/historial-lotes-envio${queryString ? `?${queryString}` : ''}`; const url = `${API_URL}/historial-lotes-envio${queryString ? `?${queryString}` : ''}`;
const response = await apiClient.get<LoteDeEnvioHistorialDto[]>(url); const response = await apiClient.get<LoteDeEnvioHistorialDto[]>(url);
return response.data; return response.data;
}; };

View File

@@ -33,19 +33,18 @@ apiClient.interceptors.response.use(
(error) => { (error) => {
// Cualquier código de estado que este fuera del rango de 2xx causa la ejecución de esta función // Cualquier código de estado que este fuera del rango de 2xx causa la ejecución de esta función
if (axios.isAxiosError(error) && error.response) { if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 401) { // Verificamos si la petición fallida NO fue al endpoint de login.
// Token inválido o expirado const isLoginAttempt = error.config?.url?.endsWith('/auth/login');
console.warn("Error 401: Token inválido o expirado. Deslogueando...");
// Solo activamos el deslogueo automático si el error 401 NO es de un intento de login.
if (error.response.status === 401 && !isLoginAttempt) {
console.warn("Error 401 (Token inválido o expirado) detectado. Deslogueando...");
// Limpiar localStorage y recargar la página.
// AuthContext se encargará de redirigir a /login al recargar porque no encontrará token.
localStorage.removeItem('authToken'); localStorage.removeItem('authToken');
localStorage.removeItem('authUser'); // Asegurar limpiar también el usuario localStorage.removeItem('authUser');
// Forzar un hard refresh para que AuthContext se reinicialice y redirija
// Esto también limpiará cualquier estado de React.
// --- Mostrar mensaje antes de redirigir ---
alert("Tu sesión ha expirado o no es válida. Serás redirigido a la página de inicio de sesión."); alert("Tu sesión ha expirado o no es válida. Serás redirigido a la página de inicio de sesión.");
window.location.href = '/login'; // Redirección más directa window.location.href = '/login';
} }
} }
// Es importante devolver el error para que el componente que hizo la llamada pueda manejarlo también si es necesario // Es importante devolver el error para que el componente que hizo la llamada pueda manejarlo también si es necesario

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
@@ -60,6 +60,22 @@ El sistema está organizado en varios módulos clave para cubrir todas las área
- **Autenticación Segura:** Mediante JSON Web Tokens (JWT). - **Autenticación Segura:** Mediante JSON Web Tokens (JWT).
- **Auditoría:** Todas las modificaciones a los datos maestros y transacciones importantes se registran en tablas de historial (`_H`). - **Auditoría:** Todas las modificaciones a los datos maestros y transacciones importantes se registran en tablas de historial (`_H`).
### 📨 Suscripciones
- **Gestión de Suscriptores:** ABM completo de clientes, incluyendo datos de contacto, dirección de entrega y forma de pago preferida.
- **Ciclo de Vida de la Suscripción:** Creación y administración de suscripciones por cliente y publicación, con fechas de inicio, fin, días de entrega y estados (`Activa`, `Pausada`, `Cancelada`).
- **Facturación Proporcional (Pro-rata):** El sistema genera automáticamente una "Factura de Alta" por el monto proporcional cuando un cliente se suscribe en un período ya cerrado, evitando cobros excesivos en la primera factura.
- **Gestión de Promociones:** ABM de promociones (ej. descuentos porcentuales, bonificación de días) y asignación a suscripciones específicas con vigencia definida.
- **Cuenta Corriente del Suscriptor:** Administración de ajustes manuales (`Crédito`/`Débito`) para manejar situaciones excepcionales como notas de crédito, devoluciones o cargos especiales.
- **Procesos de Cierre Mensual:**
- **Generación de Cierre:** Proceso masivo que calcula y genera todas las facturas del período, aplicando promociones y ajustes.
- **Notificaciones Automáticas:** Envío automático de resúmenes de cuenta por email a cada suscriptor al generar el cierre.
- **Gestión de Débito Automático:**
- **Generación de Archivo:** Creación del archivo de texto plano en formato "Pago Directo" para el Banco Galicia. Las "Facturas de Alta" se excluyen automáticamente de este proceso.
- **Procesamiento de Respuesta:** Herramienta para procesar el archivo de respuesta del banco, actualizando los estados de pago (`Pagada`/`Rechazada`) de forma masiva.
- **Auditoría de Comunicaciones:**
- **Log de Envíos:** Registro detallado de cada correo electrónico enviado (individual o masivo), incluyendo estado (`Enviado`/`Fallido`) y mensajes de error.
- **Historial de Envíos:** Interfaz para consultar el historial de notificaciones enviadas por cada factura o por cada lote de cierre mensual.
--- ---
## 🛠️ Stack Tecnológico ## 🛠️ Stack Tecnológico

View File

@@ -7,9 +7,14 @@ services:
- shared-net - shared-net
environment: environment:
- DB_SA_PASSWORD=${DB_SA_PASSWORD} - DB_SA_PASSWORD=${DB_SA_PASSWORD}
- ConnectionStrings__DefaultConnection=Server=db-sqlserver;Database=SistemaGestion;User ID=sa;Password=${DB_SA_PASSWORD};TrustServerCertificate=True; - ConnectionStrings__DefaultConnection=Server=db-sqlserver;Database=SistemaGestion;User ID=sa;Password=${DB_SA_PASSWORD};MultipleActiveResultSets=True;TrustServerCertificate=True;
ports: ports:
- "8081:8080" - "8081:8080"
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# --- Servicio del Frontend --- # --- Servicio del Frontend ---
web-gestion: web-gestion:
@@ -21,6 +26,11 @@ services:
- "8080:80" - "8080:80"
depends_on: depends_on:
- api-gestion - api-gestion
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks: networks: