feat(Reportes): Refactoriza vista Dist. General y corrige totales PDF
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 5m35s
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 5m35s
Frontend: - Se refactoriza la página `ReporteListadoDistribucionGeneralPage.tsx` para reemplazar la tabla HTML estándar por el componente `DataGrid` de MUI X. - Se implementa el cálculo y la visualización de una fila de totales para las tablas de "Resumen Diario" y "Promedios por Día", mejorando la legibilidad y consistencia con otros reportes. - Se actualiza la exportación a Excel para incluir estas nuevas filas de totales. - Se corrigen errores de tipado (TypeScript) relacionados con la importación de DTOs. Backend: - Se ajusta la lógica en `ListadoDistribucionGeneralViewModel.cs` para calcular correctamente la fila "General" de promedios en la exportación a PDF. - Anteriormente, el promedio se calculaba incorrectamente dividiendo por el total de días del mes. Ahora, se calcula un promedio real basado únicamente en los días con actividad (tirada > 0), asegurando que los datos del PDF coincidan con los de la interfaz.
This commit is contained in:
@@ -14,8 +14,6 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
|||||||
public string MesConsultado { get; set; } = string.Empty;
|
public string MesConsultado { 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");
|
||||||
|
|
||||||
// --- PROPIEDAD PARA LOS TOTALES GENERALES DE PROMEDIOS ---
|
|
||||||
// Esta propiedad calcula los promedios generales basados en los datos del resumen mensual.
|
|
||||||
public ListadoDistribucionGeneralPromedioDiaDto? PromedioGeneral
|
public ListadoDistribucionGeneralPromedioDiaDto? PromedioGeneral
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -24,21 +22,30 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Filtrar solo los días con actividad para no diluir el promedio.
|
||||||
|
var diasActivos = ResumenMensual.Where(r => r.CantidadTirada > 0).ToList();
|
||||||
|
|
||||||
// Contar solo los días con tirada > 0 para promediar correctamente
|
if (!diasActivos.Any())
|
||||||
var diasConTirada = ResumenMensual.Count(d => d.CantidadTirada > 0);
|
{
|
||||||
if (diasConTirada == 0) return null;
|
return null; // No hay días con actividad, no se puede calcular el promedio.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Usar el conteo de días activos como divisor.
|
||||||
|
var totalDiasActivos = diasActivos.Count;
|
||||||
|
|
||||||
return new ListadoDistribucionGeneralPromedioDiaDto
|
return new ListadoDistribucionGeneralPromedioDiaDto
|
||||||
{
|
{
|
||||||
Dia = "General",
|
Dia = "General",
|
||||||
CantidadDias = diasConTirada,
|
CantidadDias = totalDiasActivos,
|
||||||
PromedioTirada = (int)ResumenMensual.Average(r => r.CantidadTirada),
|
// 3. Calcular el promedio real: Suma de valores / Cantidad de días activos.
|
||||||
PromedioSinCargo = (int)ResumenMensual.Average(r => r.SinCargo),
|
// Se usa división entera para que coincida con el formato sin decimales.
|
||||||
PromedioPerdidos = (int)ResumenMensual.Average(r => r.Perdidos),
|
PromedioTirada = diasActivos.Sum(r => r.CantidadTirada) / totalDiasActivos,
|
||||||
PromedioLlevados = (int)ResumenMensual.Average(r => r.Llevados),
|
PromedioSinCargo = diasActivos.Sum(r => r.SinCargo) / totalDiasActivos,
|
||||||
PromedioDevueltos = (int)ResumenMensual.Average(r => r.Devueltos),
|
PromedioPerdidos = diasActivos.Sum(r => r.Perdidos) / totalDiasActivos,
|
||||||
PromedioVendidos = (int)ResumenMensual.Average(r => r.Vendidos)
|
PromedioLlevados = diasActivos.Sum(r => r.Llevados) / totalDiasActivos,
|
||||||
|
PromedioDevueltos = diasActivos.Sum(r => r.Devueltos) / totalDiasActivos,
|
||||||
|
PromedioVendidos = diasActivos.Sum(r => r.Vendidos) / totalDiasActivos
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ namespace GestionIntegral.Api.Services.Reportes
|
|||||||
{
|
{
|
||||||
private readonly IReportesRepository _reportesRepository;
|
private readonly IReportesRepository _reportesRepository;
|
||||||
private readonly ILogger<ReportesService> _logger;
|
private readonly ILogger<ReportesService> _logger;
|
||||||
// No necesitas _connectionFactory aquí si toda la lógica de BD está en el repositorio.
|
|
||||||
|
|
||||||
public ReportesService(IReportesRepository reportesRepository, ILogger<ReportesService> logger)
|
public ReportesService(IReportesRepository reportesRepository, ILogger<ReportesService> logger)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx
|
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { DataGrid, type GridColDef, GridFooterContainer } from '@mui/x-data-grid';
|
||||||
|
import { esES } from '@mui/x-data-grid/locales';
|
||||||
import reportesService from '../../services/Reportes/reportesService';
|
import reportesService from '../../services/Reportes/reportesService';
|
||||||
import type { ListadoDistribucionGeneralResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralResponseDto';
|
import type { ListadoDistribucionGeneralResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralResponseDto';
|
||||||
|
import type { ListadoDistribucionGeneralResumenDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralResumenDto';
|
||||||
|
import type { ListadoDistribucionGeneralPromedioDiaDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralPromedioDiaDto';
|
||||||
import SeleccionaReporteListadoDistribucionGeneral from './SeleccionaReporteListadoDistribucionGeneral';
|
import SeleccionaReporteListadoDistribucionGeneral from './SeleccionaReporteListadoDistribucionGeneral';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface ResumenDiarioExtendido extends ListadoDistribucionGeneralResumenDto {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PromediosPorDiaExtendido extends ListadoDistribucionGeneralPromedioDiaDto {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||||
const [reportData, setReportData] = useState<ListadoDistribucionGeneralResponseDto | null>(null);
|
const [reportData, setReportData] = useState<ListadoDistribucionGeneralResponseDto | null>(null);
|
||||||
|
const [resumenCalculado, setResumenCalculado] = useState<ResumenDiarioExtendido[]>([]);
|
||||||
|
const [promediosCalculado, setPromediosCalculado] = useState<PromediosPorDiaExtendido[]>([]);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -18,12 +32,32 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
|||||||
const [showParamSelector, setShowParamSelector] = useState(true);
|
const [showParamSelector, setShowParamSelector] = useState(true);
|
||||||
const [currentParams, setCurrentParams] = useState<{
|
const [currentParams, setCurrentParams] = useState<{
|
||||||
idPublicacion: number;
|
idPublicacion: number;
|
||||||
fechaDesde: string; // Primer día del mes
|
fechaDesde: string;
|
||||||
fechaHasta: string; // Último día del mes
|
fechaHasta: string;
|
||||||
nombrePublicacion?: string; // Para el nombre del archivo
|
nombrePublicacion?: string;
|
||||||
mesAnioParaNombreArchivo?: string; // Para el nombre del archivo (ej. YYYY-MM)
|
mesAnioParaNombreArchivo?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
const [totalesResumen, setTotalesResumen] = useState({
|
||||||
|
cantidadTirada: 0,
|
||||||
|
sinCargo: 0,
|
||||||
|
perdidos: 0,
|
||||||
|
llevados: 0,
|
||||||
|
devueltos: 0,
|
||||||
|
vendidos: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [totalesPromedios, setTotalesPromedios] = useState({
|
||||||
|
cantidadDias: 0,
|
||||||
|
promedioTirada: 0,
|
||||||
|
promedioSinCargo: 0,
|
||||||
|
promedioPerdidos: 0,
|
||||||
|
promedioLlevados: 0,
|
||||||
|
promedioDevueltos: 0,
|
||||||
|
promedioVendidos: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const handleGenerarReporte = useCallback(async (params: {
|
const handleGenerarReporte = useCallback(async (params: {
|
||||||
idPublicacion: number;
|
idPublicacion: number;
|
||||||
fechaDesde: string;
|
fechaDesde: string;
|
||||||
@@ -32,19 +66,67 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setApiErrorParams(null);
|
setApiErrorParams(null);
|
||||||
|
setReportData(null);
|
||||||
// Para el nombre del archivo y título del PDF
|
setResumenCalculado([]);
|
||||||
|
setPromediosCalculado([]);
|
||||||
|
setTotalesResumen({ cantidadTirada: 0, sinCargo: 0, perdidos: 0, llevados: 0, devueltos: 0, vendidos: 0 });
|
||||||
|
setTotalesPromedios({ cantidadDias: 0, promedioTirada: 0, promedioSinCargo: 0, promedioPerdidos: 0, promedioLlevados: 0, promedioDevueltos: 0, promedioVendidos: 0 });
|
||||||
|
|
||||||
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
|
const pubService = (await import('../../services/Distribucion/publicacionService')).default;
|
||||||
const pubData = await pubService.getPublicacionById(params.idPublicacion);
|
const pubData = await pubService.getPublicacionById(params.idPublicacion);
|
||||||
const mesAnioParts = params.fechaDesde.split('-'); // YYYY-MM-DD -> [YYYY, MM, DD]
|
const mesAnioParts = params.fechaDesde.split('-');
|
||||||
const mesAnioNombre = `${mesAnioParts[1]}/${mesAnioParts[0]}`;
|
const mesAnioNombre = `${mesAnioParts[1]}/${mesAnioParts[0]}`;
|
||||||
|
|
||||||
|
setCurrentParams({ ...params, nombrePublicacion: pubData?.nombre, mesAnioParaNombreArchivo: mesAnioNombre });
|
||||||
|
|
||||||
setCurrentParams({...params, nombrePublicacion: pubData?.nombre, mesAnioParaNombreArchivo: mesAnioNombre });
|
|
||||||
try {
|
try {
|
||||||
const data = await reportesService.getListadoDistribucionGeneral(params);
|
const data = await reportesService.getListadoDistribucionGeneral(params);
|
||||||
|
|
||||||
|
const resumenConId = data.resumen.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
id: `resumen-${index}`
|
||||||
|
}));
|
||||||
|
setResumenCalculado(resumenConId);
|
||||||
|
|
||||||
|
const totalesResumenCalculados = data.resumen.reduce((acc, item) => ({
|
||||||
|
cantidadTirada: acc.cantidadTirada + item.cantidadTirada,
|
||||||
|
sinCargo: acc.sinCargo + item.sinCargo,
|
||||||
|
perdidos: acc.perdidos + item.perdidos,
|
||||||
|
llevados: acc.llevados + item.llevados,
|
||||||
|
devueltos: acc.devueltos + item.devueltos,
|
||||||
|
vendidos: acc.vendidos + item.vendidos,
|
||||||
|
}), { cantidadTirada: 0, sinCargo: 0, perdidos: 0, llevados: 0, devueltos: 0, vendidos: 0 });
|
||||||
|
setTotalesResumen(totalesResumenCalculados);
|
||||||
|
|
||||||
|
const promediosConId = data.promediosPorDia.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
id: `promedio-${index}`
|
||||||
|
}));
|
||||||
|
setPromediosCalculado(promediosConId);
|
||||||
|
|
||||||
|
const totalDias = data.promediosPorDia.reduce((sum, item) => sum + item.cantidadDias, 0);
|
||||||
|
const promediosPonderados = data.promediosPorDia.reduce((acc, item) => ({
|
||||||
|
tirada: acc.tirada + item.promedioTirada * item.cantidadDias,
|
||||||
|
sinCargo: acc.sinCargo + item.promedioSinCargo * item.cantidadDias,
|
||||||
|
perdidos: acc.perdidos + item.promedioPerdidos * item.cantidadDias,
|
||||||
|
llevados: acc.llevados + item.promedioLlevados * item.cantidadDias,
|
||||||
|
devueltos: acc.devueltos + item.promedioDevueltos * item.cantidadDias,
|
||||||
|
vendidos: acc.vendidos + item.promedioVendidos * item.cantidadDias,
|
||||||
|
}), { tirada: 0, sinCargo: 0, perdidos: 0, llevados: 0, devueltos: 0, vendidos: 0 });
|
||||||
|
|
||||||
|
setTotalesPromedios({
|
||||||
|
cantidadDias: totalDias,
|
||||||
|
promedioTirada: totalDias > 0 ? promediosPonderados.tirada / totalDias : 0,
|
||||||
|
promedioSinCargo: totalDias > 0 ? promediosPonderados.sinCargo / totalDias : 0,
|
||||||
|
promedioPerdidos: totalDias > 0 ? promediosPonderados.perdidos / totalDias : 0,
|
||||||
|
promedioLlevados: totalDias > 0 ? promediosPonderados.llevados / totalDias : 0,
|
||||||
|
promedioDevueltos: totalDias > 0 ? promediosPonderados.devueltos / totalDias : 0,
|
||||||
|
promedioVendidos: totalDias > 0 ? promediosPonderados.vendidos / totalDias : 0,
|
||||||
|
});
|
||||||
|
|
||||||
setReportData(data);
|
setReportData(data);
|
||||||
if ((!data.resumen || data.resumen.length === 0) && (!data.promediosPorDia || data.promediosPorDia.length === 0)) {
|
|
||||||
|
if (resumenConId.length === 0 && promediosConId.length === 0) {
|
||||||
setError("No se encontraron datos para los parámetros seleccionados.");
|
setError("No se encontraron datos para los parámetros seleccionados.");
|
||||||
}
|
}
|
||||||
setShowParamSelector(false);
|
setShowParamSelector(false);
|
||||||
@@ -65,17 +147,19 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
setApiErrorParams(null);
|
setApiErrorParams(null);
|
||||||
setCurrentParams(null);
|
setCurrentParams(null);
|
||||||
|
setResumenCalculado([]);
|
||||||
|
setPromediosCalculado([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleExportToExcel = useCallback(() => {
|
const handleExportToExcel = useCallback(() => {
|
||||||
if (!reportData || (!reportData.resumen?.length && !reportData.promediosPorDia?.length)) {
|
if (!resumenCalculado.length && !promediosCalculado.length) {
|
||||||
alert("No hay datos para exportar.");
|
alert("No hay datos para exportar.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wb = XLSX.utils.book_new();
|
const wb = XLSX.utils.book_new();
|
||||||
|
|
||||||
if (reportData.resumen?.length) {
|
if (resumenCalculado.length) {
|
||||||
const resumenToExport = reportData.resumen.map(item => ({
|
const resumenToExport = resumenCalculado.map(item => ({
|
||||||
"Fecha": item.fecha ? new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-',
|
"Fecha": item.fecha ? new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-',
|
||||||
"Tirada": item.cantidadTirada,
|
"Tirada": item.cantidadTirada,
|
||||||
"Sin Cargo": item.sinCargo,
|
"Sin Cargo": item.sinCargo,
|
||||||
@@ -84,12 +168,21 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
|||||||
"Devueltos": item.devueltos,
|
"Devueltos": item.devueltos,
|
||||||
"Vendidos": item.vendidos,
|
"Vendidos": item.vendidos,
|
||||||
}));
|
}));
|
||||||
|
resumenToExport.push({
|
||||||
|
"Fecha": "TOTALES",
|
||||||
|
"Tirada": totalesResumen.cantidadTirada,
|
||||||
|
"Sin Cargo": totalesResumen.sinCargo,
|
||||||
|
"Perdidos": totalesResumen.perdidos,
|
||||||
|
"Llevados": totalesResumen.llevados,
|
||||||
|
"Devueltos": totalesResumen.devueltos,
|
||||||
|
"Vendidos": totalesResumen.vendidos,
|
||||||
|
});
|
||||||
const wsResumen = XLSX.utils.json_to_sheet(resumenToExport);
|
const wsResumen = XLSX.utils.json_to_sheet(resumenToExport);
|
||||||
XLSX.utils.book_append_sheet(wb, wsResumen, "ResumenDiario");
|
XLSX.utils.book_append_sheet(wb, wsResumen, "ResumenDiario");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reportData.promediosPorDia?.length) {
|
if (promediosCalculado.length) {
|
||||||
const promediosToExport = reportData.promediosPorDia.map(item => ({
|
const promediosToExport = promediosCalculado.map(item => ({
|
||||||
"Día Semana": item.dia,
|
"Día Semana": item.dia,
|
||||||
"Cant. Días": item.cantidadDias,
|
"Cant. Días": item.cantidadDias,
|
||||||
"Prom. Tirada": item.promedioTirada,
|
"Prom. Tirada": item.promedioTirada,
|
||||||
@@ -99,6 +192,16 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
|||||||
"Prom. Devueltos": item.promedioDevueltos,
|
"Prom. Devueltos": item.promedioDevueltos,
|
||||||
"Prom. Vendidos": item.promedioVendidos,
|
"Prom. Vendidos": item.promedioVendidos,
|
||||||
}));
|
}));
|
||||||
|
promediosToExport.push({
|
||||||
|
"Día Semana": "GENERAL",
|
||||||
|
"Cant. Días": totalesPromedios.cantidadDias,
|
||||||
|
"Prom. Tirada": totalesPromedios.promedioTirada,
|
||||||
|
"Prom. Sin Cargo": totalesPromedios.promedioSinCargo,
|
||||||
|
"Prom. Perdidos": totalesPromedios.promedioPerdidos,
|
||||||
|
"Prom. Llevados": totalesPromedios.promedioLlevados,
|
||||||
|
"Prom. Devueltos": totalesPromedios.promedioDevueltos,
|
||||||
|
"Prom. Vendidos": totalesPromedios.promedioVendidos,
|
||||||
|
});
|
||||||
const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport);
|
const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport);
|
||||||
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDia");
|
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDia");
|
||||||
}
|
}
|
||||||
@@ -110,7 +213,7 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
fileName += ".xlsx";
|
fileName += ".xlsx";
|
||||||
XLSX.writeFile(wb, fileName);
|
XLSX.writeFile(wb, fileName);
|
||||||
}, [reportData, currentParams]);
|
}, [resumenCalculado, promediosCalculado, currentParams, totalesResumen, totalesPromedios]);
|
||||||
|
|
||||||
const handleGenerarYAbrirPdf = useCallback(async () => {
|
const handleGenerarYAbrirPdf = useCallback(async () => {
|
||||||
if (!currentParams) {
|
if (!currentParams) {
|
||||||
@@ -122,8 +225,8 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const blob = await reportesService.getListadoDistribucionGeneralPdf({
|
const blob = await reportesService.getListadoDistribucionGeneralPdf({
|
||||||
idPublicacion: currentParams.idPublicacion,
|
idPublicacion: currentParams.idPublicacion,
|
||||||
fechaDesde: currentParams.fechaDesde, // El servicio y SP esperan fechaDesde para el mes/año
|
fechaDesde: currentParams.fechaDesde,
|
||||||
fechaHasta: currentParams.fechaHasta // El SP no usa esta, pero el servicio de reporte sí para el nombre
|
fechaHasta: currentParams.fechaHasta
|
||||||
});
|
});
|
||||||
if (blob.type === "application/json") {
|
if (blob.type === "application/json") {
|
||||||
const text = await blob.text();
|
const text = await blob.text();
|
||||||
@@ -141,6 +244,56 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [currentParams]);
|
}, [currentParams]);
|
||||||
|
|
||||||
|
const columnsResumen: GridColDef[] = [
|
||||||
|
{ field: 'fecha', headerName: 'Fecha', flex: 0.8, minWidth: 100, valueFormatter: (value) => value ? new Date(value).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-' },
|
||||||
|
{ field: 'cantidadTirada', headerName: 'Tirada', type: 'number', flex: 1, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||||
|
{ field: 'sinCargo', headerName: 'Sin Cargo', type: 'number', flex: 1, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||||
|
{ field: 'perdidos', headerName: 'Perdidos', type: 'number', flex: 1, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||||
|
{ field: 'llevados', headerName: 'Llevados', type: 'number', flex: 1, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||||
|
{ field: 'devueltos', headerName: 'Devueltos', type: 'number', flex: 1, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||||
|
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', flex: 1, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const columnsPromedios: GridColDef[] = [
|
||||||
|
{ field: 'dia', headerName: 'Día Semana', flex: 1, minWidth: 130 },
|
||||||
|
{ field: 'cantidadDias', headerName: 'Cant. Días', type: 'number', flex: 0.8, minWidth: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR') },
|
||||||
|
{ field: 'promedioTirada', headerName: 'Prom. Tirada', type: 'number', flex: 1, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR', { maximumFractionDigits: 0 }) },
|
||||||
|
{ field: 'promedioSinCargo', headerName: 'Prom. Sin Cargo', type: 'number', flex: 1, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR', { maximumFractionDigits: 0 }) },
|
||||||
|
{ field: 'promedioPerdidos', headerName: 'Prom. Perdidos', type: 'number', flex: 1, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR', { maximumFractionDigits: 0 }) },
|
||||||
|
{ field: 'promedioLlevados', headerName: 'Prom. Llevados', type: 'number', flex: 1, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR', { maximumFractionDigits: 0 }) },
|
||||||
|
{ field: 'promedioDevueltos', headerName: 'Prom. Devueltos', type: 'number', flex: 1, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR', { maximumFractionDigits: 0 }) },
|
||||||
|
{ field: 'promedioVendidos', headerName: 'Prom. Vendidos', type: 'number', flex: 1, minWidth: 120, align: 'right', headerAlign: 'right', valueFormatter: (value) => Number(value).toLocaleString('es-AR', { maximumFractionDigits: 0 }) },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CustomFooterResumen = () => (
|
||||||
|
<GridFooterContainer>
|
||||||
|
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', width: '100%' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ minWidth: columnsResumen[0].minWidth, fontWeight: 'bold' }}>TOTALES:</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsResumen[1].flex, minWidth: columnsResumen[1].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesResumen.cantidadTirada.toLocaleString('es-AR')}</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsResumen[2].flex, minWidth: columnsResumen[2].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesResumen.sinCargo.toLocaleString('es-AR')}</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsResumen[3].flex, minWidth: columnsResumen[3].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesResumen.perdidos.toLocaleString('es-AR')}</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsResumen[4].flex, minWidth: columnsResumen[4].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesResumen.llevados.toLocaleString('es-AR')}</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsResumen[5].flex, minWidth: columnsResumen[5].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesResumen.devueltos.toLocaleString('es-AR')}</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsResumen[6].flex, minWidth: columnsResumen[6].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesResumen.vendidos.toLocaleString('es-AR')}</Typography>
|
||||||
|
</Box>
|
||||||
|
</GridFooterContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CustomFooterPromedios = () => (
|
||||||
|
<GridFooterContainer>
|
||||||
|
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', width: '100%' }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[0].flex, minWidth: columnsPromedios[0].minWidth, fontWeight: 'bold' }}>GENERAL:</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[1].flex, minWidth: columnsPromedios[1].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.cantidadDias.toLocaleString('es-AR')}</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[2].flex, minWidth: columnsPromedios[2].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.promedioTirada.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[3].flex, minWidth: columnsPromedios[3].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.promedioSinCargo.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[4].flex, minWidth: columnsPromedios[4].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.promedioPerdidos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[5].flex, minWidth: columnsPromedios[5].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.promedioLlevados.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[6].flex, minWidth: columnsPromedios[6].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.promedioDevueltos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||||
|
<Typography variant="subtitle2" sx={{ flex: columnsPromedios[7].flex, minWidth: columnsPromedios[7].minWidth, textAlign: 'right', fontWeight: 'bold' }}>{totalesPromedios.promedioVendidos.toLocaleString('es-AR', { maximumFractionDigits: 0 })}</Typography>
|
||||||
|
</Box>
|
||||||
|
</GridFooterContainer>
|
||||||
|
);
|
||||||
|
|
||||||
if (showParamSelector) {
|
if (showParamSelector) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||||
@@ -179,70 +332,34 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
|||||||
{!loading && !error && reportData && (
|
{!loading && !error && reportData && (
|
||||||
<>
|
<>
|
||||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Resumen Diario</Typography>
|
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Resumen Diario</Typography>
|
||||||
{reportData.resumen && reportData.resumen.length > 0 ? (
|
{resumenCalculado.length > 0 ? (
|
||||||
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 3 }}>
|
<Paper sx={{ height: 450, width: '100%', mb: 3 }}>
|
||||||
<Table stickyHeader size="small">
|
<DataGrid
|
||||||
<TableHead>
|
rows={resumenCalculado}
|
||||||
<TableRow>
|
columns={columnsResumen}
|
||||||
<TableCell>Fecha</TableCell>
|
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||||
<TableCell align="right">Tirada</TableCell>
|
density="compact"
|
||||||
<TableCell align="right">Sin Cargo</TableCell>
|
slots={{ footer: CustomFooterResumen }}
|
||||||
<TableCell align="right">Perdidos</TableCell>
|
hideFooterPagination
|
||||||
<TableCell align="right">Llevados</TableCell>
|
hideFooterSelectedRowCount
|
||||||
<TableCell align="right">Devueltos</TableCell>
|
/>
|
||||||
<TableCell align="right">Vendidos</TableCell>
|
</Paper>
|
||||||
</TableRow>
|
) : (<Typography sx={{ fontStyle: 'italic' }}>No hay datos de resumen diario.</Typography>)}
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{reportData.resumen.map((row, idx) => (
|
|
||||||
<TableRow key={`resumen-${idx}`}>
|
|
||||||
<TableCell>{row.fecha ? new Date(row.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-'}</TableCell>
|
|
||||||
<TableCell align="right">{row.cantidadTirada.toLocaleString('es-AR')}</TableCell>
|
|
||||||
<TableCell align="right">{row.sinCargo.toLocaleString('es-AR')}</TableCell>
|
|
||||||
<TableCell align="right">{row.perdidos.toLocaleString('es-AR')}</TableCell>
|
|
||||||
<TableCell align="right">{row.llevados.toLocaleString('es-AR')}</TableCell>
|
|
||||||
<TableCell align="right">{row.devueltos.toLocaleString('es-AR')}</TableCell>
|
|
||||||
<TableCell align="right">{row.vendidos.toLocaleString('es-AR')}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
) : (<Typography>No hay datos de resumen diario.</Typography>)}
|
|
||||||
|
|
||||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Promedios por Día de Semana</Typography>
|
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Promedios por Día de Semana</Typography>
|
||||||
{reportData.promediosPorDia && reportData.promediosPorDia.length > 0 ? (
|
{promediosCalculado.length > 0 ? (
|
||||||
<TableContainer component={Paper} sx={{ maxHeight: '300px' }}>
|
<Paper sx={{ height: 400, width: '100%' }}>
|
||||||
<Table stickyHeader size="small">
|
<DataGrid
|
||||||
<TableHead>
|
rows={promediosCalculado}
|
||||||
<TableRow>
|
columns={columnsPromedios}
|
||||||
<TableCell>Día</TableCell>
|
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||||
<TableCell align="right">Cant. Días</TableCell>
|
density="compact"
|
||||||
<TableCell align="right">Prom. Tirada</TableCell>
|
slots={{ footer: CustomFooterPromedios }}
|
||||||
<TableCell align="right">Prom. Sin Cargo</TableCell>
|
hideFooterPagination
|
||||||
<TableCell align="right">Prom. Perdidos</TableCell>
|
hideFooterSelectedRowCount
|
||||||
<TableCell align="right">Prom. Llevados</TableCell>
|
/>
|
||||||
<TableCell align="right">Prom. Devueltos</TableCell>
|
</Paper>
|
||||||
<TableCell align="right">Prom. Vendidos</TableCell>
|
) : (<Typography sx={{ fontStyle: 'italic' }}>No hay datos de promedios por día.</Typography>)}
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{reportData.promediosPorDia.map((row, idx) => (
|
|
||||||
<TableRow key={`promedio-${idx}`}>
|
|
||||||
<TableCell>{row.dia}</TableCell>
|
|
||||||
<TableCell align="right">{row.cantidadDias.toLocaleString('es-AR')}</TableCell>
|
|
||||||
<TableCell align="right">{row.promedioTirada.toLocaleString('es-AR')}</TableCell>
|
|
||||||
<TableCell align="right">{row.promedioSinCargo.toLocaleString('es-AR')}</TableCell>
|
|
||||||
<TableCell align="right">{row.promedioPerdidos.toLocaleString('es-AR')}</TableCell>
|
|
||||||
<TableCell align="right">{row.promedioLlevados.toLocaleString('es-AR')}</TableCell>
|
|
||||||
<TableCell align="right">{row.promedioDevueltos.toLocaleString('es-AR')}</TableCell>
|
|
||||||
<TableCell align="right">{row.promedioVendidos.toLocaleString('es-AR')}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
) : (<Typography>No hay datos de promedios por día.</Typography>)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ El sistema está organizado en varios módulos clave para cubrir todas las área
|
|||||||
- **Acceso a Datos:** Dapper (Micro ORM)
|
- **Acceso a Datos:** Dapper (Micro ORM)
|
||||||
- **Base de Datos:** Microsoft SQL Server
|
- **Base de Datos:** Microsoft SQL Server
|
||||||
- **Autenticación:** JWT Bearer Tokens
|
- **Autenticación:** JWT Bearer Tokens
|
||||||
- **Reportes PDF:** `Microsoft.ReportingServices.ReportViewerControl.WebForms` (a través de wrappers para .NET Core)
|
- **Reportes PDF:** QuestPDF C# PDF Generation Library
|
||||||
- **Exportación Excel:** NPOI
|
- **Exportación Excel:** NPOI
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
@@ -158,6 +158,8 @@ cd GestionIntegralWeb
|
|||||||
### Backend (`Backend/GestionIntegral.Api/`)
|
### Backend (`Backend/GestionIntegral.Api/`)
|
||||||
```
|
```
|
||||||
/Controllers/ # Controladores de la API, organizados por módulo
|
/Controllers/ # Controladores de la API, organizados por módulo
|
||||||
|
Reportes/
|
||||||
|
PdfTemplates/ # Templates de los reportes utilizados mediante QuestPDF
|
||||||
/Data/
|
/Data/
|
||||||
/Repositories/ # Lógica de acceso a datos con Dapper para cada entidad
|
/Repositories/ # Lógica de acceso a datos con Dapper para cada entidad
|
||||||
DbConnectionFactory.cs # Clase para crear conexiones a la BD
|
DbConnectionFactory.cs # Clase para crear conexiones a la BD
|
||||||
|
|||||||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
services:
|
||||||
|
# --- Servicio del Backend ---
|
||||||
|
api-gestion:
|
||||||
|
image: dmolinari/gestionintegralweb-backend:latest
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- shared-net
|
||||||
|
environment:
|
||||||
|
- DB_SA_PASSWORD=${DB_SA_PASSWORD}
|
||||||
|
- ConnectionStrings__DefaultConnection=Server=db-sqlserver;Database=SistemaGestion;User ID=sa;Password=${DB_SA_PASSWORD};TrustServerCertificate=True;
|
||||||
|
ports:
|
||||||
|
- "8081:8080"
|
||||||
|
|
||||||
|
# --- Servicio del Frontend ---
|
||||||
|
web-gestion:
|
||||||
|
image: dmolinari/gestionintegralweb-frontend:latest
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- shared-net
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
depends_on:
|
||||||
|
- api-gestion
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
# Nos conectamos a la red que creará el otro stack
|
||||||
|
shared-net:
|
||||||
|
external: true
|
||||||
Reference in New Issue
Block a user