QuestPdf Implementado en la totalidad de reportes.
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 7m55s

This commit is contained in:
2025-06-24 12:52:37 -03:00
parent a5bcbefa52
commit 229eb937f5
51 changed files with 4009 additions and 954 deletions

View File

@@ -1,4 +1,3 @@
// src/pages/Reportes/ReporteListadoDistMensualPage.tsx
import React, { useState, useCallback, useMemo } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button,

View File

@@ -1,116 +1,50 @@
// src/pages/Reportes/ReporteDetalleDistribucionCanillasPage.tsx
import React, { useState, useCallback, useMemo } from 'react';
import React, { useState, useCallback } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button
Box, Typography, Paper, CircularProgress, Alert, Button,
TableContainer, Table, TableHead, TableRow, TableCell, TableBody
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter } from '@mui/x-data-grid';
import { esES } from '@mui/x-data-grid/locales';
import reportesService from '../../services/Reportes/reportesService';
import type { ReporteDistribucionCanillasResponseDto } from '../../models/dtos/Reportes/ReporteDistribucionCanillasResponseDto';
import SeleccionaReporteDetalleDistribucionCanillas from './SeleccionaReporteDetalleDistribucionCanillas';
import type { ListadoDistribucionGeneralResponseDto } from '../../models/dtos/Reportes/ListadoDistribucionGeneralResponseDto';
import SeleccionaReporteListadoDistribucionGeneral from './SeleccionaReporteListadoDistribucionGeneral';
import * as XLSX from 'xlsx';
import axios from 'axios';
interface TotalesComunes {
totalCantSalida: number;
totalCantEntrada: number;
vendidos: number;
totalRendir: number;
}
const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
const [reportData, setReportData] = useState<ReporteDistribucionCanillasResponseDto | null>(null);
const ReporteListadoDistribucionGeneralPage: React.FC = () => {
const [reportData, setReportData] = useState<ListadoDistribucionGeneralResponseDto | null>(null);
const [loading, setLoading] = useState(false);
const [loadingPdf, setLoadingPdf] = useState(false);
const [error, setError] = useState<string | null>(null);
const [apiErrorParams, setApiErrorParams] = useState<string | null>(null);
const [showParamSelector, setShowParamSelector] = useState(true);
const [currentParams, setCurrentParams] = useState<{
fecha: string;
idEmpresa: number;
nombreEmpresa?: string;
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)
} | null>(null);
const [pdfSoloTotales, setPdfSoloTotales] = useState(false);
// Estados para los totales de cada sección
const initialTotals: TotalesComunes = { totalCantSalida: 0, totalCantEntrada: 0, vendidos: 0, totalRendir: 0 };
const [totalesCanillas, setTotalesCanillas] = useState<TotalesComunes>(initialTotals);
const [totalesAccionistas, setTotalesAccionistas] = useState<TotalesComunes>(initialTotals);
const [totalesTodos, setTotalesTodos] = useState<TotalesComunes>(initialTotals);
const [totalesCanillasOtraFecha, setTotalesCanillasOtraFecha] = useState<TotalesComunes>(initialTotals);
const [totalesAccionistasOtraFecha, setTotalesAccionistasOtraFecha] = useState<TotalesComunes>(initialTotals);
// --- Formateadores ---
const currencyFormatter = (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '';
const numberFormatter = (value: number | null | undefined) =>
value != null ? Number(value).toLocaleString('es-AR') : '';
const calculateAndSetTotals = (dataArray: Array<any> | undefined, setTotalsFunc: React.Dispatch<React.SetStateAction<TotalesComunes>>) => {
if (dataArray && dataArray.length > 0) {
const totals = dataArray.reduce((acc, item) => {
acc.totalCantSalida += Number(item.totalCantSalida) || 0;
acc.totalCantEntrada += Number(item.totalCantEntrada) || 0;
acc.totalRendir += Number(item.totalRendir) || 0;
return acc;
}, { totalCantSalida: 0, totalCantEntrada: 0, totalRendir: 0 });
totals.vendidos = totals.totalCantSalida - totals.totalCantEntrada;
setTotalsFunc(totals);
} else {
setTotalsFunc(initialTotals);
}
};
const handleGenerarReporte = useCallback(async (params: {
fecha: string;
idEmpresa: number;
idPublicacion: number;
fechaDesde: string;
fechaHasta: string;
}) => {
setLoading(true);
setError(null);
setApiErrorParams(null);
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
const empData = await empresaService.getEmpresaById(params.idEmpresa);
setCurrentParams({ ...params, nombreEmpresa: empData?.nombre });
setReportData(null); // Limpiar datos antiguos
// Resetear totales
setTotalesCanillas(initialTotals);
setTotalesAccionistas(initialTotals);
setTotalesTodos(initialTotals);
setTotalesCanillasOtraFecha(initialTotals);
setTotalesAccionistasOtraFecha(initialTotals);
// Para el nombre del archivo y título del PDF
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 mesAnioNombre = `${mesAnioParts[1]}/${mesAnioParts[0]}`;
setCurrentParams({...params, nombrePublicacion: pubData?.nombre, mesAnioParaNombreArchivo: mesAnioNombre });
try {
const data = await reportesService.getReporteDistribucionCanillas(params);
const addIds = <T extends Record<string, any>>(arr: T[] | undefined, prefix: string): Array<T & { id: string }> =>
(arr || []).map((item, index) => ({ ...item, id: `${prefix}-${item.publicacion || item.tipoVendedor || 'item'}-${index}-${Math.random().toString(36).substring(7)}` }));
const processedData = {
canillas: addIds(data.canillas, 'can'),
canillasAccionistas: addIds(data.canillasAccionistas, 'acc'),
canillasTodos: addIds(data.canillasTodos, 'all'),
canillasLiquidadasOtraFecha: addIds(data.canillasLiquidadasOtraFecha, 'canliq'),
canillasAccionistasLiquidadasOtraFecha: addIds(data.canillasAccionistasLiquidadasOtraFecha, 'accliq'),
controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'),
controlDevolucionesRemitos: addIds(data.controlDevolucionesRemitos, 'cdr'),
controlDevolucionesOtrosDias: addIds(data.controlDevolucionesOtrosDias, 'cdo')
};
setReportData(processedData);
// Calcular y setear totales para cada sección
calculateAndSetTotals(processedData.canillas, setTotalesCanillas);
calculateAndSetTotals(processedData.canillasAccionistas, setTotalesAccionistas);
calculateAndSetTotals(processedData.canillasTodos, setTotalesTodos);
calculateAndSetTotals(processedData.canillasLiquidadasOtraFecha, setTotalesCanillasOtraFecha);
calculateAndSetTotals(processedData.canillasAccionistasLiquidadasOtraFecha, setTotalesAccionistasOtraFecha);
const noData = (!data.canillas || data.canillas.length === 0) &&
(!data.canillasAccionistas || data.canillasAccionistas.length === 0) &&
(!data.canillasTodos || data.canillasTodos.length === 0); // Podrías añadir más chequeos si es necesario
if (noData) {
const data = await reportesService.getListadoDistribucionGeneral(params);
setReportData(data);
if ((!data.resumen || data.resumen.length === 0) && (!data.promediosPorDia || data.promediosPorDia.length === 0)) {
setError("No se encontraron datos para los parámetros seleccionados.");
}
setShowParamSelector(false);
@@ -134,96 +68,62 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
}, []);
const handleExportToExcel = useCallback(() => {
if (!reportData) {
if (!reportData || (!reportData.resumen?.length && !reportData.promediosPorDia?.length)) {
alert("No hay datos para exportar.");
return;
}
const wb = XLSX.utils.book_new();
const formatAndSheet = (
data: any[],
sheetName: string,
fields: Record<string, string>,
totals?: TotalesComunes
) => {
if (data && data.length > 0) {
let exportedData = data.map(item => {
const row: Record<string, any> = {};
const { id, ...itemData } = item; // Excluir el 'id' generado
Object.keys(fields).forEach(key => {
row[fields[key]] = (itemData as any)[key];
if (key === 'fecha' && (itemData as any)[key]) {
row[fields[key]] = new Date((itemData as any)[key]).toLocaleDateString('es-AR', { timeZone: 'UTC' });
}
if ((key === 'totalRendir') && (itemData as any)[key] != null) {
row[fields[key]] = parseFloat((itemData as any)[key]); // Mantener como número para suma en Excel
}
if (key === 'vendidos' && itemData.totalCantSalida != null && itemData.totalCantEntrada != null) {
row[fields[key]] = itemData.totalCantSalida - itemData.totalCantEntrada;
}
});
return row;
});
if (reportData.resumen?.length) {
const resumenToExport = reportData.resumen.map(item => ({
"Fecha": item.fecha ? new Date(item.fecha).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-',
"Tirada": item.cantidadTirada,
"Sin Cargo": item.sinCargo,
"Perdidos": item.perdidos,
"Llevados": item.llevados,
"Devueltos": item.devueltos,
"Vendidos": item.vendidos,
}));
const wsResumen = XLSX.utils.json_to_sheet(resumenToExport);
XLSX.utils.book_append_sheet(wb, wsResumen, "ResumenDiario");
}
if (totals) {
const totalRow: Record<string, any> = {};
const fieldKeys = Object.keys(fields);
totalRow[fields[fieldKeys[0]]] = "TOTALES"; // Título en la primera columna
if (fields.totalCantSalida) totalRow[fields.totalCantSalida] = totals.totalCantSalida;
if (fields.totalCantEntrada) totalRow[fields.totalCantEntrada] = totals.totalCantEntrada;
if (fields.vendidos) totalRow[fields.vendidos] = totals.vendidos;
if (fields.totalRendir) totalRow[fields.totalRendir] = totals.totalRendir;
exportedData.push(totalRow);
}
if (reportData.promediosPorDia?.length) {
const promediosToExport = reportData.promediosPorDia.map(item => ({
"Día Semana": item.dia,
"Cant. Días": item.cantidadDias,
"Prom. Tirada": item.promedioTirada,
"Prom. Sin Cargo": item.promedioSinCargo,
"Prom. Perdidos": item.promedioPerdidos,
"Prom. Llevados": item.promedioLlevados,
"Prom. Devueltos": item.promedioDevueltos,
"Prom. Vendidos": item.promedioVendidos,
}));
const wsPromedios = XLSX.utils.json_to_sheet(promediosToExport);
XLSX.utils.book_append_sheet(wb, wsPromedios, "PromediosPorDia");
}
const ws = XLSX.utils.json_to_sheet(exportedData);
const headers = Object.values(fields);
ws['!cols'] = headers.map(h => {
const maxLen = Math.max(...exportedData.map(row => (row[h]?.toString() ?? '').length), h.length);
return { wch: maxLen + 2 };
});
ws['!freeze'] = { xSplit: 0, ySplit: 1 }; // Congelar primera fila
XLSX.utils.book_append_sheet(wb, ws, sheetName);
}
};
const fieldsCanillaAccionista = { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsTodos = { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsCtrlDevDetalle = { ingresados: "Ingresados", sobrantes: "Sobrantes", sinCargo: "Sin Cargo", publicacion: "Publicación", llevados: "Llevados", devueltos: "Devueltos", tipo: "Tipo" };
const fieldsCtrlDevRemitos = { remito: "Remito Ingresado" };
const fieldsCtrlDevOtrosDias = { devueltos: "Devueltos Otros Días" };
formatAndSheet(reportData.canillas, "Canillitas_Dia", fieldsCanillaAccionista, totalesCanillas);
formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", fieldsCanillaAccionista, totalesAccionistas);
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos, totalesTodos);
formatAndSheet(reportData.canillasLiquidadasOtraFecha, "Canillitas_OtrasFechas", fieldsCanillaAccionistaFechaLiq, totalesCanillasOtraFecha);
formatAndSheet(reportData.canillasAccionistasLiquidadasOtraFecha, "Accionistas_OtrasFechas", fieldsCanillaAccionistaFechaLiq, totalesAccionistasOtraFecha);
formatAndSheet(reportData.controlDevolucionesDetalle, "CtrlDev_Detalle", fieldsCtrlDevDetalle); // Sin totales para estos
formatAndSheet(reportData.controlDevolucionesRemitos, "CtrlDev_Remitos", fieldsCtrlDevRemitos);
formatAndSheet(reportData.controlDevolucionesOtrosDias, "CtrlDev_OtrosDias", fieldsCtrlDevOtrosDias);
let fileName = "ReporteDetalleDistribucionCanillitas";
let fileName = "ListadoDistribucionGeneral";
if (currentParams) {
fileName += `_${currentParams.nombreEmpresa?.replace(/\s+/g, '') ?? `Emp${currentParams.idEmpresa}`}`;
fileName += `_${currentParams.fecha}`;
fileName += `_${currentParams.nombrePublicacion?.replace(/\s+/g, '') ?? `Pub${currentParams.idPublicacion}`}`;
fileName += `_${currentParams.mesAnioParaNombreArchivo?.replace('/', '-')}`;
}
fileName += ".xlsx";
XLSX.writeFile(wb, fileName);
}, [reportData, currentParams, totalesCanillas, totalesAccionistas, totalesTodos, totalesCanillasOtraFecha, totalesAccionistasOtraFecha]);
}, [reportData, currentParams]);
const handleGenerarYAbrirPdf = useCallback(async (soloTotales: boolean) => {
const handleGenerarYAbrirPdf = useCallback(async () => {
if (!currentParams) {
setError("Primero debe generar el reporte en pantalla o seleccionar parámetros.");
return;
}
setLoadingPdf(true);
setError(null);
setPdfSoloTotales(soloTotales);
try {
const blob = await reportesService.getReporteDistribucionCanillasPdf({
...currentParams,
soloTotales
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
});
if (blob.type === "application/json") {
const text = await blob.text();
@@ -241,99 +141,13 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
}
}, [currentParams]);
// Definiciones de columnas
const commonColumns: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.3 },
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
];
const commonColumnsWithFecha: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1 },
{ field: 'canilla', headerName: 'Canillita', width: 220, flex: 1.1 },
{ field: 'fecha', headerName: 'Fecha Mov.', width: 120, flex: 0.7, valueFormatter: (value) => value ? new Date(value as string).toLocaleDateString('es-AR', { timeZone: 'UTC' }) : '-' },
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
];
const columnsTodos: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.2 },
{ field: 'tipoVendedor', headerName: 'Tipo Vendedor', width: 150, flex: 0.8 },
{ field: 'totalCantSalida', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalCantEntrada', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (value) => numberFormatter(Number(value))},
{ field: 'vendidos', headerName: 'Vendidos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueGetter: (_value, row) => (row.totalCantSalida || 0) - (row.totalCantEntrada || 0), valueFormatter: (value) => numberFormatter(Number(value)) },
{ field: 'totalRendir', headerName: 'A Rendir', type: 'number', width: 150, align: 'right', headerAlign: 'right', valueFormatter: (value) => currencyFormatter(Number(value)) },
];
const columnsCtrlDevDetalle: GridColDef[] = [
{ field: 'publicacion', headerName: 'Publicación', width: 200, flex: 1.5 },
{ field: 'tipo', headerName: 'Tipo', width: 100, flex: 0.8 },
{ field: 'ingresados', headerName: 'Ingresados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
{ field: 'sobrantes', headerName: 'Sobrantes', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
{ field: 'sinCargo', headerName: 'Sin Cargo', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
{ field: 'llevados', headerName: 'Llevados', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
{ field: 'devueltos', headerName: 'Devueltos', type: 'number', width: 100, align: 'right', headerAlign: 'right', valueFormatter: (v) => numberFormatter(Number(v))},
];
const columnsCtrlDevRemitos: GridColDef[] = [
{ field: 'remito', headerName: 'Remito Ingresado', flex: 1 },
];
const columnsCtrlDevOtrosDias: GridColDef[] = [
{ field: 'devueltos', headerName: 'Devueltos Otros Días', flex: 1 },
];
// Memoizar filas (los IDs ya se añaden en handleGenerarReporte)
const rowsCanillas = useMemo(() => reportData?.canillas ?? [], [reportData]);
const rowsAccionistas = useMemo(() => reportData?.canillasAccionistas ?? [], [reportData]);
const rowsTodos = useMemo(() => reportData?.canillasTodos ?? [], [reportData]);
const rowsCanillasOtraFecha = useMemo(() => reportData?.canillasLiquidadasOtraFecha ?? [], [reportData]);
const rowsAccionistasOtraFecha = useMemo(() => reportData?.canillasAccionistasLiquidadasOtraFecha ?? [], [reportData]);
const rowsCtrlDevDetalle = useMemo(() => reportData?.controlDevolucionesDetalle ?? [], [reportData]);
const rowsCtrlDevRemitos = useMemo(() => reportData?.controlDevolucionesRemitos ?? [], [reportData]);
const rowsCtrlDevOtrosDias = useMemo(() => reportData?.controlDevolucionesOtrosDias ?? [], [reportData]);
// --- Custom Footers ---
// eslint-disable-next-line react/display-name
const createCustomFooter = (totals: TotalesComunes, columns: GridColDef[]) => () => (
<GridFooterContainer sx={{ justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0, minWidth: '300px' }}>
<GridFooter sx={{ borderTop: 'none' }} />
</Box>
<Box sx={{ p: 1, display: 'flex', alignItems: 'center', fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto' }}>
<Typography variant="subtitle2" sx={{ flex: columns[0].flex, width: columns[0].width, textAlign: 'right', fontWeight: 'bold' }}>TOTALES:</Typography>
{columns[1].field !== 'tipoVendedor' && <Typography variant="subtitle2" sx={{ flex: columns[1].flex, width: columns[1].width, textAlign: 'right', fontWeight: 'bold', pr:1 }}></Typography> /* Placeholder for Canilla/Tipo */ }
{columns[1].field === 'tipoVendedor' && <Typography variant="subtitle2" sx={{ flex: columns[1].flex, width: columns[1].width, textAlign: 'right', fontWeight: 'bold', pr:1 }}></Typography> /* Placeholder for Canilla/Tipo */ }
{columns.find(c => c.field === 'fecha') && <Typography variant="subtitle2" sx={{ flex: columns.find(c=>c.field === 'fecha')?.flex, width: columns.find(c=>c.field === 'fecha')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}></Typography> /* Placeholder for Fecha */}
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'totalCantSalida')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}>{numberFormatter(totals.totalCantSalida)}</Typography>
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'totalCantEntrada')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}>{numberFormatter(totals.totalCantEntrada)}</Typography>
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'vendidos')?.width, textAlign: 'right', fontWeight: 'bold', pr:1 }}>{numberFormatter(totals.vendidos)}</Typography>
<Typography variant="subtitle2" sx={{ width: columns.find(c=>c.field === 'totalRendir')?.width, textAlign: 'right', fontWeight: 'bold' }}>{currencyFormatter(totals.totalRendir)}</Typography>
</Box>
</GridFooterContainer>
);
const CustomFooterCanillas = useMemo(() => createCustomFooter(totalesCanillas, commonColumns), [totalesCanillas]);
const CustomFooterAccionistas = useMemo(() => createCustomFooter(totalesAccionistas, commonColumns), [totalesAccionistas]);
const CustomFooterTodos = useMemo(() => createCustomFooter(totalesTodos, columnsTodos), [totalesTodos]);
const CustomFooterCanillasOtraFecha = useMemo(() => createCustomFooter(totalesCanillasOtraFecha, commonColumnsWithFecha), [totalesCanillasOtraFecha]);
const CustomFooterAccionistasOtraFecha = useMemo(() => createCustomFooter(totalesAccionistasOtraFecha, commonColumnsWithFecha), [totalesAccionistasOtraFecha]);
if (showParamSelector) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', mt: 2 }}>
<Paper sx={{ width: '100%', maxWidth: 600 }} elevation={3}>
<SeleccionaReporteDetalleDistribucionCanillas
<SeleccionaReporteListadoDistribucionGeneral
onGenerarReporte={handleGenerarReporte}
onCancel={handleVolverAParametros} // Asumo que no se usa, ya que el selector no tiene botón de cancelar
onCancel={handleVolverAParametros}
isLoading={loading}
apiErrorMessage={apiErrorParams}
/>
@@ -345,13 +159,10 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
return (
<Box sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, flexWrap: 'wrap', gap: 1 }}>
<Typography variant="h5">Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha}</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Button onClick={() => handleGenerarYAbrirPdf(false)} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf && !pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Detalle"}
</Button>
<Button onClick={() => handleGenerarYAbrirPdf(true)} variant="contained" color="secondary" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf && pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Totales"}
<Typography variant="h5">Reporte: Listado Distribución General</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button onClick={handleGenerarYAbrirPdf} variant="contained" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf ? <CircularProgress size={20} color="inherit" /> : "Abrir PDF"}
</Button>
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small">
Exportar a Excel
@@ -362,151 +173,80 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{loading && <Box sx={{ textAlign: 'center' }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData && (
<>
{/* Canillitas (del día) */}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Canillitas (del día)</Typography>
{rowsCanillas.length > 0 ? (
<Paper sx={{ height: 400, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={rowsCanillas}
columns={commonColumns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterCanillas }}
hideFooterSelectedRowCount
/>
</Paper>
) : (<Typography sx={{ fontStyle: 'italic', mb:2 }}>No hay datos para canillitas (del día).</Typography>)}
{/* Accionistas (del día) */}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Accionistas (del día)</Typography>
{rowsAccionistas.length > 0 ? (
<Paper sx={{ height: 400, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={rowsAccionistas}
columns={commonColumns}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterAccionistas }}
hideFooterSelectedRowCount
/>
</Paper>
) : (<Typography sx={{ fontStyle: 'italic', mb:2 }}>No hay datos para accionistas (del día).</Typography>)}
{/* Resumen por Tipo de Vendedor (del día) */}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Resumen por Tipo de Vendedor (del día)</Typography>
{rowsTodos.length > 0 ? (
<Paper sx={{ height: 300, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={rowsTodos}
columns={columnsTodos}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterTodos }}
hideFooterSelectedRowCount
/>
</Paper>
) : (<Typography sx={{ fontStyle: 'italic', mb:2 }}>No hay datos para resumen por tipo de vendedor (del día).</Typography>)}
{/* Canillitas (Liquidados de Otras Fechas) */}
{rowsCanillasOtraFecha.length > 0 && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Canillitas (Liquidados de Otras Fechas)</Typography>
<Paper sx={{ height: 300, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={rowsCanillasOtraFecha}
columns={commonColumnsWithFecha}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterCanillasOtraFecha }}
hideFooterSelectedRowCount
/>
</Paper>
</>
)}
{/* Accionistas (Liquidados de Otras Fechas) */}
{rowsAccionistasOtraFecha.length > 0 && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Accionistas (Liquidados de Otras Fechas)</Typography>
<Paper sx={{ height: 300, width: '100%', mb: 3, '& .MuiDataGrid-footerContainer': { minHeight: '52px' } }}>
<DataGrid
rows={rowsAccionistasOtraFecha}
columns={commonColumnsWithFecha}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
slots={{ footer: CustomFooterAccionistasOtraFecha }}
hideFooterSelectedRowCount
/>
</Paper>
</>
)}
{/* Control Devoluciones - Detalle */}
{rowsCtrlDevDetalle.length > 0 && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Control Devoluciones - Detalle</Typography>
<Paper sx={{ height: 300, width: '100%', mb: 3 }}>
<DataGrid
rows={rowsCtrlDevDetalle}
columns={columnsCtrlDevDetalle}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
hideFooterSelectedRowCount // Sin footer personalizado para estos
/>
</Paper>
</>
)}
{/* Control Devoluciones - Remitos */}
{rowsCtrlDevRemitos.length > 0 && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Control Devoluciones - Remitos Ingresados</Typography>
<Paper sx={{ height: 200, width: '100%', mb: 3 }}>
<DataGrid
rows={rowsCtrlDevRemitos}
columns={columnsCtrlDevRemitos}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
autoHeight
hideFooterSelectedRowCount
/>
</Paper>
</>
)}
{/* Control Devoluciones - Otros Días */}
{rowsCtrlDevOtrosDias.length > 0 && (
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>Control Devoluciones - Otros Días</Typography>
<Paper sx={{ height: 200, width: '100%', mb: 3 }}>
<DataGrid
rows={rowsCtrlDevOtrosDias}
columns={columnsCtrlDevOtrosDias}
localeText={esES.components.MuiDataGrid.defaultProps.localeText}
density="compact"
autoHeight
hideFooterSelectedRowCount
/>
</Paper>
</>
)}
<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>)}
<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>)}
</>
)}
{!loading && !error && (!reportData ||
(rowsCanillas.length === 0 && rowsAccionistas.length === 0 && rowsTodos.length === 0 &&
rowsCanillasOtraFecha.length === 0 && rowsAccionistasOtraFecha.length === 0 &&
rowsCtrlDevDetalle.length === 0 && rowsCtrlDevRemitos.length === 0 && rowsCtrlDevOtrosDias.length === 0
)) &&
(<Typography>No se encontraron datos para los criterios seleccionados.</Typography>)
}
</Box>
);
};
export default ReporteDetalleDistribucionCanillasPage;
export default ReporteListadoDistribucionGeneralPage;

View File

@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import {
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl,
ToggleButtonGroup, ToggleButton, RadioGroup, FormControlLabel, Radio
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl,
ToggleButtonGroup, ToggleButton, RadioGroup, FormControlLabel, Radio
} from '@mui/material';
export type TipoListadoDistMensual = 'diarios' | 'publicaciones';
@@ -71,35 +71,79 @@ const SeleccionaReporteListadoDistMensual: React.FC<SeleccionaReporteListadoDist
disabled={isLoading}
InputLabelProps={{ shrink: true }}
/>
<Box sx={{ mt: 2, mb: 2, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Typography variant="subtitle1" sx={{ mb: 0, fontWeight: 500 }}>
Tipo de Vendedor
</Typography>
<FormControl component="fieldset" margin="normal" fullWidth disabled={isLoading} sx={{ mt: 2, mb: 2, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<FormControl component="fieldset" margin="normal" fullWidth disabled={isLoading}>
<Typography component="legend" variant="subtitle2" sx={{mb:0.5, color: 'rgba(0, 0, 0, 0.6)'}}>Tipo de Vendedor</Typography>
<ToggleButtonGroup
color="primary"
<ToggleButtonGroup
value={esAccionista ? 'accionistas' : 'canillitas'}
exclusive
onChange={(_, newValue) => {
if (newValue !== null) setEsAccionista(newValue === 'accionistas');
onChange={(_, value) => {
if (value !== null) setEsAccionista(value === 'accionistas');
}}
aria-label="Tipo de reporte"
disabled={isLoading}
color="primary"
size="large"
sx={{
borderRadius: 2,
boxShadow: 2,
backgroundColor: '#f5f5f5',
p: 0.5,
}}
aria-label="Tipo de Vendedor"
size="small"
>
<ToggleButton value="canillitas">Canillitas</ToggleButton>
<ToggleButton value="accionistas">Accionistas</ToggleButton>
</ToggleButtonGroup>
</FormControl>
<ToggleButton
value="canillitas"
aria-label="Canillitas"
sx={{
fontWeight: esAccionista ? 400 : 700,
bgcolor: !esAccionista ? 'primary.main' : 'background.paper',
color: !esAccionista ? 'primary.contrastText' : 'text.primary',
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'primary.contrastText',
},
minWidth: 140,
borderRadius: 2,
mx: 1,
}}
>
Canillitas
</ToggleButton>
<ToggleButton
value="accionistas"
aria-label="Accionistas"
sx={{
fontWeight: esAccionista ? 700 : 400,
bgcolor: esAccionista ? 'primary.main' : 'background.paper',
color: esAccionista ? 'primary.contrastText' : 'text.primary',
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'primary.contrastText',
},
minWidth: 140,
borderRadius: 2,
mx: 1,
}}
>
Accionistas
</ToggleButton>
</ToggleButtonGroup>
</FormControl>
</Box>
<FormControl component="fieldset" margin="normal" fullWidth disabled={isLoading}>
<Typography component="legend" variant="subtitle2" sx={{mb:0.5, color: 'rgba(0, 0, 0, 0.6)'}}>Variante del Reporte</Typography>
<Typography component="legend" variant="subtitle2" sx={{ mb: 0.5, color: 'rgba(0, 0, 0, 0.6)' }}>Variante del Reporte</Typography>
<RadioGroup
row
aria-label="Variante del Reporte"
name="tipoReporte"
value={tipoReporte}
onChange={(e) => setTipoReporte(e.target.value as TipoListadoDistMensual)}
row
aria-label="Variante del Reporte"
name="tipoReporte"
value={tipoReporte}
onChange={(e) => setTipoReporte(e.target.value as TipoListadoDistMensual)}
>
<FormControlLabel value="publicaciones" control={<Radio size="small" />} label="Por Publicación" />
<FormControlLabel value="diarios" control={<Radio size="small" />} label="Por Diarios (El Día/El Plata)" />
<FormControlLabel value="publicaciones" control={<Radio size="small" />} label="Por Publicación" />
<FormControlLabel value="diarios" control={<Radio size="small" />} label="Por Diarios (El Día/El Plata)" />
</RadioGroup>
</FormControl>

View File

@@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react';
import {
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem,
ToggleButtonGroup,
ToggleButton
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem,
ToggleButtonGroup,
ToggleButton
} from '@mui/material';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import publicacionService from '../../services/Distribucion/publicacionService';
@@ -90,7 +90,7 @@ const SeleccionaReporteListadoDistribucionCanillasImporte: React.FC<SeleccionaRe
<MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>
))}
</Select>
{localErrors.idPublicacion && <Typography color="error" variant="caption" sx={{ml:1.5}}>{localErrors.idPublicacion}</Typography>}
{localErrors.idPublicacion && <Typography color="error" variant="caption" sx={{ ml: 1.5 }}>{localErrors.idPublicacion}</Typography>}
</FormControl>
<TextField
label="Fecha Desde"
@@ -119,64 +119,64 @@ const SeleccionaReporteListadoDistribucionCanillasImporte: React.FC<SeleccionaRe
InputLabelProps={{ shrink: true }}
/>
<Box sx={{ mt: 2, mb: 2, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Typography variant="subtitle1" sx={{ mb: 1, fontWeight: 500 }}>
Tipo de reporte
</Typography>
<ToggleButtonGroup
value={esAccionista ? 'accionistas' : 'canillitas'}
exclusive
onChange={(_, value) => {
if (value !== null) setEsAccionista(value === 'accionistas');
}}
aria-label="Tipo de reporte"
disabled={isLoading}
color="primary"
size="large"
sx={{
borderRadius: 2,
boxShadow: 2,
backgroundColor: '#f5f5f5',
p: 0.5,
}}
>
<ToggleButton
value="canillitas"
aria-label="Canillitas"
sx={{
fontWeight: esAccionista ? 400 : 700,
bgcolor: !esAccionista ? 'primary.main' : 'background.paper',
color: !esAccionista ? 'primary.contrastText' : 'text.primary',
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'primary.contrastText',
},
minWidth: 140,
borderRadius: 2,
mx: 1,
}}
>
Canillitas
</ToggleButton>
<ToggleButton
value="accionistas"
aria-label="Accionistas"
sx={{
fontWeight: esAccionista ? 700 : 400,
bgcolor: esAccionista ? 'primary.main' : 'background.paper',
color: esAccionista ? 'primary.contrastText' : 'text.primary',
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'primary.contrastText',
},
minWidth: 140,
borderRadius: 2,
mx: 1,
}}
>
Accionistas
</ToggleButton>
</ToggleButtonGroup>
</Box>
<Typography variant="subtitle1" sx={{ mb: 1, fontWeight: 500 }}>
Tipo de Vendedor
</Typography>
<ToggleButtonGroup
value={esAccionista ? 'accionistas' : 'canillitas'}
exclusive
onChange={(_, value) => {
if (value !== null) setEsAccionista(value === 'accionistas');
}}
aria-label="Tipo de Vendedor"
disabled={isLoading}
color="primary"
size="large"
sx={{
borderRadius: 2,
boxShadow: 2,
backgroundColor: '#f5f5f5',
p: 0.5,
}}
>
<ToggleButton
value="canillitas"
aria-label="Canillitas"
sx={{
fontWeight: esAccionista ? 400 : 700,
bgcolor: !esAccionista ? 'primary.main' : 'background.paper',
color: !esAccionista ? 'primary.contrastText' : 'text.primary',
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'primary.contrastText',
},
minWidth: 140,
borderRadius: 2,
mx: 1,
}}
>
Canillitas
</ToggleButton>
<ToggleButton
value="accionistas"
aria-label="Accionistas"
sx={{
fontWeight: esAccionista ? 700 : 400,
bgcolor: esAccionista ? 'primary.main' : 'background.paper',
color: esAccionista ? 'primary.contrastText' : 'text.primary',
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'primary.contrastText',
},
minWidth: 140,
borderRadius: 2,
mx: 1,
}}
>
Accionistas
</ToggleButton>
</ToggleButtonGroup>
</Box>
{apiErrorMessage && <Alert severity="error" sx={{ mt: 2 }}>{apiErrorMessage}</Alert>}
{localErrors.dropdowns && <Alert severity="warning" sx={{ mt: 1 }}>{localErrors.dropdowns}</Alert>}

View File

@@ -1,4 +1,3 @@
// src/pages/Reportes/SeleccionaReporteListadoDistribucionGeneral.tsx
import React, { useState, useEffect } from 'react';
import {
Box, Typography, Button, CircularProgress, Alert,