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 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
|
||||
{
|
||||
get
|
||||
@@ -24,21 +22,30 @@ namespace GestionIntegral.Api.Dtos.Reportes.ViewModels
|
||||
{
|
||||
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
|
||||
var diasConTirada = ResumenMensual.Count(d => d.CantidadTirada > 0);
|
||||
if (diasConTirada == 0) return null;
|
||||
if (!diasActivos.Any())
|
||||
{
|
||||
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
|
||||
{
|
||||
Dia = "General",
|
||||
CantidadDias = diasConTirada,
|
||||
PromedioTirada = (int)ResumenMensual.Average(r => r.CantidadTirada),
|
||||
PromedioSinCargo = (int)ResumenMensual.Average(r => r.SinCargo),
|
||||
PromedioPerdidos = (int)ResumenMensual.Average(r => r.Perdidos),
|
||||
PromedioLlevados = (int)ResumenMensual.Average(r => r.Llevados),
|
||||
PromedioDevueltos = (int)ResumenMensual.Average(r => r.Devueltos),
|
||||
PromedioVendidos = (int)ResumenMensual.Average(r => r.Vendidos)
|
||||
CantidadDias = totalDiasActivos,
|
||||
// 3. Calcular el promedio real: Suma de valores / Cantidad de días activos.
|
||||
// Se usa división entera para que coincida con el formato sin decimales.
|
||||
PromedioTirada = diasActivos.Sum(r => r.CantidadTirada) / totalDiasActivos,
|
||||
PromedioSinCargo = diasActivos.Sum(r => r.SinCargo) / totalDiasActivos,
|
||||
PromedioPerdidos = diasActivos.Sum(r => r.Perdidos) / totalDiasActivos,
|
||||
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 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)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// src/pages/Reportes/ReporteListadoDistribucionCanillasPage.tsx
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button,
|
||||
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
|
||||
Box, Typography, Paper, CircularProgress, Alert, Button
|
||||
} 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 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 * as XLSX from 'xlsx';
|
||||
import axios from 'axios';
|
||||
|
||||
interface ResumenDiarioExtendido extends ListadoDistribucionGeneralResumenDto {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface PromediosPorDiaExtendido extends ListadoDistribucionGeneralPromedioDiaDto {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
const [reportData, setReportData] = useState<ListadoDistribucionGeneralResponseDto | null>(null);
|
||||
const [resumenCalculado, setResumenCalculado] = useState<ResumenDiarioExtendido[]>([]);
|
||||
const [promediosCalculado, setPromediosCalculado] = useState<PromediosPorDiaExtendido[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPdf, setLoadingPdf] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -18,12 +32,32 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
const [showParamSelector, setShowParamSelector] = useState(true);
|
||||
const [currentParams, setCurrentParams] = useState<{
|
||||
idPublicacion: number;
|
||||
fechaDesde: string; // Primer día del mes
|
||||
fechaHasta: string; // Último día del mes
|
||||
nombrePublicacion?: string; // Para el nombre del archivo
|
||||
mesAnioParaNombreArchivo?: string; // Para el nombre del archivo (ej. YYYY-MM)
|
||||
fechaDesde: string;
|
||||
fechaHasta: string;
|
||||
nombrePublicacion?: string;
|
||||
mesAnioParaNombreArchivo?: string;
|
||||
} | 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: {
|
||||
idPublicacion: number;
|
||||
fechaDesde: string;
|
||||
@@ -32,19 +66,67 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
|
||||
// Para el nombre del archivo y título del PDF
|
||||
setReportData(null);
|
||||
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 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]}`;
|
||||
|
||||
setCurrentParams({ ...params, nombrePublicacion: pubData?.nombre, mesAnioParaNombreArchivo: mesAnioNombre });
|
||||
|
||||
setCurrentParams({...params, nombrePublicacion: pubData?.nombre, mesAnioParaNombreArchivo: mesAnioNombre });
|
||||
try {
|
||||
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);
|
||||
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.");
|
||||
}
|
||||
setShowParamSelector(false);
|
||||
@@ -65,17 +147,19 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
setError(null);
|
||||
setApiErrorParams(null);
|
||||
setCurrentParams(null);
|
||||
setResumenCalculado([]);
|
||||
setPromediosCalculado([]);
|
||||
}, []);
|
||||
|
||||
const handleExportToExcel = useCallback(() => {
|
||||
if (!reportData || (!reportData.resumen?.length && !reportData.promediosPorDia?.length)) {
|
||||
if (!resumenCalculado.length && !promediosCalculado.length) {
|
||||
alert("No hay datos para exportar.");
|
||||
return;
|
||||
}
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
if (reportData.resumen?.length) {
|
||||
const resumenToExport = reportData.resumen.map(item => ({
|
||||
if (resumenCalculado.length) {
|
||||
const resumenToExport = resumenCalculado.map(item => ({
|
||||
"Fecha": item.fecha ? new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-',
|
||||
"Tirada": item.cantidadTirada,
|
||||
"Sin Cargo": item.sinCargo,
|
||||
@@ -84,12 +168,21 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
"Devueltos": item.devueltos,
|
||||
"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);
|
||||
XLSX.utils.book_append_sheet(wb, wsResumen, "ResumenDiario");
|
||||
}
|
||||
|
||||
if (reportData.promediosPorDia?.length) {
|
||||
const promediosToExport = reportData.promediosPorDia.map(item => ({
|
||||
if (promediosCalculado.length) {
|
||||
const promediosToExport = promediosCalculado.map(item => ({
|
||||
"Día Semana": item.dia,
|
||||
"Cant. Días": item.cantidadDias,
|
||||
"Prom. Tirada": item.promedioTirada,
|
||||
@@ -99,6 +192,16 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
"Prom. Devueltos": item.promedioDevueltos,
|
||||
"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);
|
||||
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDia");
|
||||
}
|
||||
@@ -110,7 +213,7 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
}
|
||||
fileName += ".xlsx";
|
||||
XLSX.writeFile(wb, fileName);
|
||||
}, [reportData, currentParams]);
|
||||
}, [resumenCalculado, promediosCalculado, currentParams, totalesResumen, totalesPromedios]);
|
||||
|
||||
const handleGenerarYAbrirPdf = useCallback(async () => {
|
||||
if (!currentParams) {
|
||||
@@ -122,8 +225,8 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
try {
|
||||
const blob = await reportesService.getListadoDistribucionGeneralPdf({
|
||||
idPublicacion: currentParams.idPublicacion,
|
||||
fechaDesde: currentParams.fechaDesde, // El servicio y SP esperan fechaDesde para el mes/año
|
||||
fechaHasta: currentParams.fechaHasta // El SP no usa esta, pero el servicio de reporte sí para el nombre
|
||||
fechaDesde: currentParams.fechaDesde,
|
||||
fechaHasta: currentParams.fechaHasta
|
||||
});
|
||||
if (blob.type === "application/json") {
|
||||
const text = await blob.text();
|
||||
@@ -141,6 +244,56 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
}
|
||||
}, [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) {
|
||||
return (
|
||||
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
@@ -179,70 +332,34 @@ const ReporteListadoDistribucionGeneralPage: React.FC = () => {
|
||||
{!loading && !error && reportData && (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Resumen Diario</Typography>
|
||||
{reportData.resumen && reportData.resumen.length > 0 ? (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '300px', mb: 3 }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Fecha</TableCell>
|
||||
<TableCell align="right">Tirada</TableCell>
|
||||
<TableCell align="right">Sin Cargo</TableCell>
|
||||
<TableCell align="right">Perdidos</TableCell>
|
||||
<TableCell align="right">Llevados</TableCell>
|
||||
<TableCell align="right">Devueltos</TableCell>
|
||||
<TableCell align="right">Vendidos</TableCell>
|
||||
</TableRow>
|
||||
</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>)}
|
||||
{resumenCalculado.length > 0 ? (
|
||||
<Paper sx={{ height: 450, width: '100%', mb: 3 }}>
|
||||
<DataGrid
|
||||
rows={resumenCalculado}
|
||||
columns={columnsResumen}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
slots={{ footer: CustomFooterResumen }}
|
||||
hideFooterPagination
|
||||
hideFooterSelectedRowCount
|
||||
/>
|
||||
</Paper>
|
||||
) : (<Typography sx={{ fontStyle: 'italic' }}>No hay datos de resumen diario.</Typography>)}
|
||||
|
||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Promedios por Día de Semana</Typography>
|
||||
{reportData.promediosPorDia && reportData.promediosPorDia.length > 0 ? (
|
||||
<TableContainer component={Paper} sx={{ maxHeight: '300px' }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Día</TableCell>
|
||||
<TableCell align="right">Cant. Días</TableCell>
|
||||
<TableCell align="right">Prom. Tirada</TableCell>
|
||||
<TableCell align="right">Prom. Sin Cargo</TableCell>
|
||||
<TableCell align="right">Prom. Perdidos</TableCell>
|
||||
<TableCell align="right">Prom. Llevados</TableCell>
|
||||
<TableCell align="right">Prom. Devueltos</TableCell>
|
||||
<TableCell align="right">Prom. Vendidos</TableCell>
|
||||
</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>)}
|
||||
{promediosCalculado.length > 0 ? (
|
||||
<Paper sx={{ height: 400, width: '100%' }}>
|
||||
<DataGrid
|
||||
rows={promediosCalculado}
|
||||
columns={columnsPromedios}
|
||||
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
|
||||
density="compact"
|
||||
slots={{ footer: CustomFooterPromedios }}
|
||||
hideFooterPagination
|
||||
hideFooterSelectedRowCount
|
||||
/>
|
||||
</Paper>
|
||||
) : (<Typography sx={{ fontStyle: 'italic' }}>No hay datos de promedios por día.</Typography>)}
|
||||
</>
|
||||
)}
|
||||
</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)
|
||||
- **Base de Datos:** Microsoft SQL Server
|
||||
- **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
|
||||
|
||||
### Frontend
|
||||
@@ -158,6 +158,8 @@ cd GestionIntegralWeb
|
||||
### Backend (`Backend/GestionIntegral.Api/`)
|
||||
```
|
||||
/Controllers/ # Controladores de la API, organizados por módulo
|
||||
Reportes/
|
||||
PdfTemplates/ # Templates de los reportes utilizados mediante QuestPDF
|
||||
/Data/
|
||||
/Repositories/ # Lógica de acceso a datos con Dapper para cada entidad
|
||||
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