Fix: Cambios solicitados. Parte 1
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 6m18s

This commit is contained in:
2025-07-18 21:46:07 -03:00
parent a35a3a66ea
commit 3e1ac6f742
10 changed files with 194 additions and 143 deletions

View File

@@ -1,15 +1,16 @@
// src/pages/Impresion/GestionarStockBobinasPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select
CircularProgress, Alert, FormControl, InputLabel, Select, FormControlLabel, Checkbox
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
import stockBobinaService from '../../services/Impresion/stockBobinaService';
import tipoBobinaService from '../../services/Impresion/tipoBobinaService';
@@ -21,8 +22,8 @@ import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateSto
import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto';
import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto';
import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto';
import type { PlantaDropdownDto } from '../../models/dtos/Impresion/PlantaDropdownDto';
import type { EstadoBobinaDropdownDto } from '../../models/dtos/Impresion/EstadoBobinaDropdownDto';
import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto';
import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto';
import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal';
import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal';
@@ -37,33 +38,39 @@ const ID_ESTADO_DANADA = 3;
const GestionarStockBobinasPage: React.FC = () => {
const [stock, setStock] = useState<StockBobinaDto[]>([]);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(false); // No carga al inicio
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Estados de los filtros
const [filtroTipoBobina, setFiltroTipoBobina] = useState<number | string>('');
const [filtroNroBobina, setFiltroNroBobina] = useState('');
const [filtroPlanta, setFiltroPlanta] = useState<number | string>('');
const [filtroEstadoBobina, setFiltroEstadoBobina] = useState<number | string>('');
const [filtroRemito, setFiltroRemito] = useState('');
const [filtroFechaHabilitado, setFiltroFechaHabilitado] = useState<boolean>(false); // <-- NUEVO
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
// Estados para datos de dropdowns
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDropdownDto[]>([]);
const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDropdownDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
// Estados de los modales
const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = useState(false);
const [selectedBobina, setSelectedBobina] = useState<StockBobinaDto | null>(null); // Para los modales
// Estado para la bobina seleccionada en un modal o menú
const [selectedBobina, setSelectedBobina] = useState<StockBobinaDto | null>(null);
// Estados para la paginación y el menú de acciones
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(25);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedBobinaForRowMenu, setSelectedBobinaForRowMenu] = useState<StockBobinaDto | null>(null); // Para el menú contextual
const [selectedBobinaForRowMenu, setSelectedBobinaForRowMenu] = useState<StockBobinaDto | null>(null);
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVer = isSuperAdmin || tienePermiso("IB001");
@@ -72,13 +79,16 @@ const GestionarStockBobinasPage: React.FC = () => {
const puedeModificarDatos = isSuperAdmin || tienePermiso("IB004");
const puedeEliminar = isSuperAdmin || tienePermiso("IB005");
const lastOpenedMenuButtonRef = useRef<HTMLButtonElement | null>(null);
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
// Asumiendo que estos servicios existen y devuelven los DTOs correctos
const [tiposData, plantasData, estadosData] = await Promise.all([
tipoBobinaService.getAllDropdownTiposBobina(),
plantaService.getPlantasForDropdown(),
estadoBobinaService.getAllDropdownEstadosBobina()
tipoBobinaService.getAllTiposBobina(),
plantaService.getAllPlantas(),
estadoBobinaService.getAllEstadosBobina()
]);
setTiposBobina(tiposData);
setPlantas(plantasData);
@@ -95,12 +105,15 @@ const GestionarStockBobinasPage: React.FC = () => {
fetchFiltersDropdownData();
}, [fetchFiltersDropdownData]);
const cargarStock = useCallback(async () => {
if (!puedeVer) {
setError("No tiene permiso para ver esta sección."); setLoading(false); return;
setError("No tiene permiso para ver esta sección.");
setLoading(false);
return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
setLoading(true);
setError(null);
setApiErrorMessage(null);
try {
const params = {
idTipoBobina: filtroTipoBobina ? Number(filtroTipoBobina) : null,
@@ -108,19 +121,39 @@ const GestionarStockBobinasPage: React.FC = () => {
idPlanta: filtroPlanta ? Number(filtroPlanta) : null,
idEstadoBobina: filtroEstadoBobina ? Number(filtroEstadoBobina) : null,
remitoFilter: filtroRemito || null,
fechaDesde: filtroFechaDesde || null,
fechaHasta: filtroFechaHasta || null,
fechaDesde: filtroFechaHabilitado ? filtroFechaDesde : null,
fechaHasta: filtroFechaHabilitado ? filtroFechaHasta : null,
};
const data = await stockBobinaService.getAllStockBobinas(params);
setStock(data);
if (data.length === 0) {
setError("No se encontraron resultados con los filtros aplicados.");
}
} catch (err) {
console.error(err); setError('Error al cargar el stock de bobinas.');
} finally { setLoading(false); }
}, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaDesde, filtroFechaHasta]);
console.error(err);
setError('Error al cargar el stock de bobinas.');
} finally {
setLoading(false);
}
}, [puedeVer, filtroTipoBobina, filtroNroBobina, filtroPlanta, filtroEstadoBobina, filtroRemito, filtroFechaHabilitado, filtroFechaDesde, filtroFechaHasta]);
useEffect(() => {
const handleBuscarClick = () => {
setPage(0); // Resetear la paginación al buscar
cargarStock();
}, [cargarStock]);
};
const handleLimpiarFiltros = () => {
setFiltroTipoBobina('');
setFiltroNroBobina('');
setFiltroPlanta('');
setFiltroEstadoBobina('');
setFiltroRemito('');
setFiltroFechaHabilitado(false);
setFiltroFechaDesde(new Date().toISOString().split('T')[0]);
setFiltroFechaHasta(new Date().toISOString().split('T')[0]);
setStock([]); // Limpiar los resultados actuales
setError(null);
};
const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); };
const handleCloseIngresoModal = () => setIngresoModalOpen(false);
@@ -139,13 +172,10 @@ const GestionarStockBobinasPage: React.FC = () => {
const handleCloseEditModal = () => {
setEditModalOpen(false);
setSelectedBobina(null);
// Devolver el foco al botón que abrió el menú (si el modal se abrió desde el menú)
if (lastOpenedMenuButtonRef.current) {
setTimeout(() => { // setTimeout puede ayudar
lastOpenedMenuButtonRef.current?.focus();
}, 0);
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
}
};
};
const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.updateDatosBobinaDisponible(idBobina, data); cargarStock(); }
@@ -158,7 +188,7 @@ const GestionarStockBobinasPage: React.FC = () => {
setApiErrorMessage(null);
setCambioEstadoModalOpen(true);
};
const handleCloseCambioEstadoModal = () => { setCambioEstadoModalOpen(false); setSelectedBobina(null); };
const handleCloseCambioEstadoModal = () => setCambioEstadoModalOpen(false);
const handleSubmitCambioEstadoModal = async (idBobina: number, data: CambiarEstadoBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.cambiarEstadoBobina(idBobina, data); cargarStock(); }
@@ -167,7 +197,6 @@ const GestionarStockBobinasPage: React.FC = () => {
const handleDeleteBobina = async (bobina: StockBobinaDto | null) => {
if (!bobina) return;
// Permitir eliminar si está Disponible (1) o Dañada (3)
if (bobina.idEstadoBobina !== ID_ESTADO_DISPONIBLE && bobina.idEstadoBobina !== ID_ESTADO_DANADA) {
alert("Solo se pueden eliminar bobinas en estado 'Disponible' o 'Dañada'.");
handleMenuClose();
@@ -181,26 +210,16 @@ const GestionarStockBobinasPage: React.FC = () => {
handleMenuClose();
};
const lastOpenedMenuButtonRef = React.useRef<HTMLButtonElement | null>(null);
const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>, bobina: StockBobinaDto) => {
setAnchorEl(event.currentTarget);
setSelectedBobinaForRowMenu(bobina);
lastOpenedMenuButtonRef.current = event.currentTarget; // Guardar el botón que abrió el menú
lastOpenedMenuButtonRef.current = event.currentTarget;
};
const handleMenuClose = () => {
setAnchorEl(null);
// No es estrictamente necesario limpiar selectedBobinaForRowMenu aquí,
// ya que se actualiza en el siguiente handleMenuOpen.
// Pero se puede ser explícito:
setSelectedBobinaForRowMenu(null);
// Devolver el foco al botón que abrió el menú si existe
if (lastOpenedMenuButtonRef.current) {
setTimeout(() => { // Pequeño retraso para asegurar que el menú se haya cerrado visualmente
lastOpenedMenuButtonRef.current?.focus();
}, 0);
setTimeout(() => { lastOpenedMenuButtonRef.current?.focus(); }, 0);
}
};
@@ -209,15 +228,26 @@ const GestionarStockBobinasPage: React.FC = () => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = stock.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
const formatDate = (dateString?: string | null) => {
if (!dateString) return '-';
const date = new Date(dateString);
if (isNaN(date.getTime())) return '-';
if (!loading && !puedeVer) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
timeZone: 'UTC'
};
return new Intl.DateTimeFormat('es-AR', options).format(date);
};
if (!puedeVer) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Stock de Bobinas</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
{/* ... (Filtros sin cambios) ... */}
<Typography variant="h6" gutterBottom>Filtros</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
@@ -243,17 +273,37 @@ const GestionarStockBobinasPage: React.FC = () => {
</Select>
</FormControl>
<TextField label="Remito" size="small" value={filtroRemito} onChange={(e) => setFiltroRemito(e.target.value)} sx={{ minWidth: 150, flexGrow: 1 }} />
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170, flexGrow: 1 }} />
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170, flexGrow: 1 }} />
</Box>
{puedeIngresar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenIngresoModal} sx={{ mt: 2 }}>Ingresar Bobina</Button>)}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
<FormControlLabel
control={<Checkbox checked={filtroFechaHabilitado} onChange={(e) => setFiltroFechaHabilitado(e.target.checked)} />}
label="Filtrar por Fechas de Remitos"
/>
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} disabled={!filtroFechaHabilitado} />
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} disabled={!filtroFechaHabilitado} />
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: 2,
mb: 2,
justifyContent: 'flex-end'
}}
>
<Button variant="contained" startIcon={<SearchIcon />} onClick={handleBuscarClick} disabled={loading}>Buscar</Button>
<Button variant="outlined" startIcon={<ClearIcon />} onClick={handleLimpiarFiltros} disabled={loading}>Limpiar Filtros</Button>
</Box>
{puedeIngresar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenIngresoModal} sx={{ ml: 'auto' }}>Ingresar Bobina</Button>)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{error && !loading && <Alert severity="warning" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeVer && (
{!loading && !error && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
@@ -262,12 +312,11 @@ const GestionarStockBobinasPage: React.FC = () => {
<TableCell>F. Remito</TableCell><TableCell>F. Estado</TableCell>
<TableCell>Publicación</TableCell><TableCell>Sección</TableCell>
<TableCell>Obs.</TableCell>
{/* Mostrar columna de acciones si tiene algún permiso de acción */}
{(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) ? 12 : 11} align="center">No se encontraron bobinas con los filtros aplicados.</TableCell></TableRow>
<TableRow><TableCell colSpan={(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) ? 12 : 11} align="center">No se encontraron bobinas con los filtros aplicados. Haga clic en "Buscar" para iniciar una consulta.</TableCell></TableRow>
) : (
displayData.map((b) => (
<TableRow key={b.idBobina} hover>
@@ -283,10 +332,9 @@ const GestionarStockBobinasPage: React.FC = () => {
{(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, b)}
// El botón de menú se deshabilita si no hay NINGUNA acción posible para esa fila
disabled={
!(b.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos) &&
!(puedeCambiarEstado) && // Siempre se puede intentar cambiar estado (el modal lo validará)
!(puedeCambiarEstado) &&
!((b.idEstadoBobina === ID_ESTADO_DISPONIBLE || b.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar)
}
><MoreVertIcon /></IconButton>
@@ -310,21 +358,17 @@ const GestionarStockBobinasPage: React.FC = () => {
<EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos
</MenuItem>
)}
{/* --- CAMBIO: Permitir cambiar estado incluso si está Dañada --- */}
{selectedBobinaForRowMenu && puedeCambiarEstado && (
<MenuItem onClick={() => { handleOpenCambioEstadoModal(selectedBobinaForRowMenu); handleMenuClose(); }}>
<SwapHorizIcon fontSize="small" sx={{ mr: 1 }} /> Cambiar Estado
</MenuItem>
)}
{/* --- CAMBIO: Permitir eliminar si está Disponible o Dañada --- */}
{selectedBobinaForRowMenu && puedeEliminar &&
(selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && (
<MenuItem onClick={() => handleDeleteBobina(selectedBobinaForRowMenu)}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar Ingreso
</MenuItem>
)}
{/* Lógica para el MenuItem "Sin acciones" */}
{selectedBobinaForRowMenu &&
!((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos)) &&
!(puedeCambiarEstado) &&
@@ -333,6 +377,7 @@ const GestionarStockBobinasPage: React.FC = () => {
}
</Menu>
{/* Modales sin cambios */}
<StockBobinaIngresoFormModal
open={ingresoModalOpen} onClose={handleCloseIngresoModal} onSubmit={handleSubmitIngresoModal}
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}

View File

@@ -1,8 +1,8 @@
import React, { useState, useCallback, useMemo, type JSXElementConstructor, type HTMLAttributes } from 'react'; // Añadido JSXElementConstructor, HTMLAttributes
import React, { useState, useCallback, useMemo, type JSXElementConstructor, type HTMLAttributes } from 'react';
import {
Box, Typography, Paper, CircularProgress, Alert, Button, type SxProps, type Theme // Añadido SxProps, Theme
Box, Typography, Paper, CircularProgress, Alert, Button, type SxProps, type Theme
} from '@mui/material';
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter, type GridSlotsComponent } from '@mui/x-data-grid'; // Añadido GridSlotsComponent
import { DataGrid, type GridColDef, GridFooterContainer, GridFooter, type GridSlotsComponent } 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';
@@ -11,7 +11,7 @@ import * as XLSX from 'xlsx';
import axios from 'axios';
// Para el tipo del footer en DataGridSectionProps
type FooterPropsOverrides = {}; // Puedes extender esto si tus footers tienen props específicos
type FooterPropsOverrides = {};
type CustomFooterType = JSXElementConstructor<HTMLAttributes<HTMLDivElement> & { sx?: SxProps<Theme> } & FooterPropsOverrides>;
@@ -39,7 +39,7 @@ const DataGridSection: React.FC<DataGridSectionProps> = ({ title, data, columns,
}
if (!rows || rows.length === 0) {
return <Typography sx={{ mt: 1, fontStyle: 'italic', mb:2 }}>No hay datos para {title.toLowerCase()}.</Typography>;
return <Typography sx={{ mt: 1, fontStyle: 'italic', mb: 2 }}>No hay datos para {title.toLowerCase()}.</Typography>;
}
const slotsProp: Partial<GridSlotsComponent> = {};
@@ -50,7 +50,7 @@ const DataGridSection: React.FC<DataGridSectionProps> = ({ title, data, columns,
return (
<>
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2, fontWeight: 'bold' }}>{title}</Typography>
<Paper sx={{ height: footerComponent ? 'auto' : height, width: '100%', mb: 2, '& .MuiDataGrid-footerContainer': { minHeight: footerComponent ? '52px' : undefined} }}>
<Paper sx={{ height: footerComponent ? 'auto' : height, width: '100%', mb: 2, '& .MuiDataGrid-footerContainer': { minHeight: footerComponent ? '52px' : undefined } }}>
<DataGrid
rows={rows}
columns={columns}
@@ -60,7 +60,7 @@ const DataGridSection: React.FC<DataGridSectionProps> = ({ title, data, columns,
slots={slotsProp} // Usar el objeto slotsProp
hideFooterSelectedRowCount={!!footerComponent}
autoHeight={!!footerComponent}
sx={!footerComponent ? {} : {
sx={!footerComponent ? {} : {
'& .MuiTablePagination-root': { display: 'none' },
'& .MuiDataGrid-selectedRowCount': { display: 'none' },
}}
@@ -90,7 +90,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
const [totalesAccionistas, setTotalesAccionistas] = useState<TotalesComunes>(initialTotals);
const [totalesCanillasOtraFecha, setTotalesCanillasOtraFecha] = useState<TotalesComunes>(initialTotals);
const [totalesAccionistasOtraFecha, setTotalesAccionistasOtraFecha] = useState<TotalesComunes>(initialTotals);
const [totalesResumen, setTotalesResumen] = useState<TotalesComunes>(initialTotals);
const currencyFormatter = (value: number | null | undefined) =>
value != null ? value.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' }) : '';
@@ -121,7 +121,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
setApiErrorParams(null);
const empresaService = (await import('../../services/Distribucion/empresaService')).default;
const empData = await empresaService.getEmpresaById(params.idEmpresa);
setCurrentParams({...params, nombreEmpresa: empData?.nombre});
setCurrentParams({ ...params, nombreEmpresa: empData?.nombre });
setReportData(null);
// Resetear totales
@@ -129,11 +129,12 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
setTotalesAccionistas(initialTotals);
setTotalesCanillasOtraFecha(initialTotals);
setTotalesAccionistasOtraFecha(initialTotals);
setTotalesResumen(initialTotals);
try {
const data = await reportesService.getReporteDistribucionCanillas(params);
const addIds = <T extends Record<string, any>>(arr: T[] | undefined, prefix: string): Array<T & { id: string }> =>
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.remito || item.devueltos || 'item'}-${index}-${Math.random().toString(36).substring(7)}` }));
const processedData = {
@@ -142,7 +143,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
canillasTodos: addIds(data.canillasTodos, 'all'), // Aún necesita IDs para DataGridSection
canillasLiquidadasOtraFecha: addIds(data.canillasLiquidadasOtraFecha, 'canliq'),
canillasAccionistasLiquidadasOtraFecha: addIds(data.canillasAccionistasLiquidadasOtraFecha, 'accliq'),
controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'),
controlDevolucionesDetalle: addIds(data.controlDevolucionesDetalle, 'cdd'),
controlDevolucionesRemitos: addIds(data.controlDevolucionesRemitos, 'cdr'),
controlDevolucionesOtrosDias: addIds(data.controlDevolucionesOtrosDias, 'cdo')
};
@@ -152,7 +153,8 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
calculateAndSetTotals(processedData.canillasAccionistas, setTotalesAccionistas);
calculateAndSetTotals(processedData.canillasLiquidadasOtraFecha, setTotalesCanillasOtraFecha);
calculateAndSetTotals(processedData.canillasAccionistasLiquidadasOtraFecha, setTotalesAccionistasOtraFecha);
calculateAndSetTotals(processedData.canillasTodos, setTotalesResumen);
const noDataFound = Object.values(processedData).every(arr => !arr || arr.length === 0);
if (noDataFound) {
setError("No se encontraron datos para los parámetros seleccionados.");
@@ -183,41 +185,41 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
const wb = XLSX.utils.book_new();
const formatAndSheet = (data: any[], sheetName: string, fields: Record<string, string>) => {
if (data && data.length > 0) {
const exportedData = data.map(item => {
const row: Record<string, any> = {};
// Excluir el 'id' generado para DataGrid si existe
const { id, ...itemData } = item;
Object.keys(fields).forEach(key => {
row[fields[key]] = (itemData as any)[key]; // Usar itemData
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]).toFixed(2);
}
if (key === 'vendidos' && itemData.totalCantSalida != null && itemData.totalCantEntrada != null) {
row[fields[key]] = itemData.totalCantSalida - itemData.totalCantEntrada;
}
});
return row;
});
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 };
XLSX.utils.book_append_sheet(wb, ws, sheetName);
}
if (data && data.length > 0) {
const exportedData = data.map(item => {
const row: Record<string, any> = {};
// Excluir el 'id' generado para DataGrid si existe
const { id, ...itemData } = item;
Object.keys(fields).forEach(key => {
row[fields[key]] = (itemData as any)[key]; // Usar itemData
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]).toFixed(2);
}
if (key === 'vendidos' && itemData.totalCantSalida != null && itemData.totalCantEntrada != null) {
row[fields[key]] = itemData.totalCantSalida - itemData.totalCantEntrada;
}
});
return row;
});
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 };
XLSX.utils.book_append_sheet(wb, ws, sheetName);
}
};
// Definición de campos para la exportación
const fieldsCanillaAccionista = { publicacion: "Publicación", canilla: "Canilla", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha:"Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsCanillaAccionistaFechaLiq = { publicacion: "Publicación", canilla: "Canilla", fecha: "Fecha Mov.", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
const fieldsTodos = { publicacion: "Publicación", tipoVendedor: "Tipo", totalCantSalida: "Llevados", totalCantEntrada: "Devueltos", vendidos: "Vendidos", totalRendir: "A Rendir" };
formatAndSheet(reportData.canillas, "Canillitas_Dia", fieldsCanillaAccionista);
formatAndSheet(reportData.canillasAccionistas, "Accionistas_Dia", fieldsCanillaAccionista);
formatAndSheet(reportData.canillasTodos, "Resumen_Dia", fieldsTodos);
@@ -273,13 +275,13 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
{ 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: '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)) },
];
@@ -288,11 +290,11 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
{ 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: '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)) },
];
// --- Custom Footers ---
const createCustomFooterComponent = (totals: TotalesComunes, columnsDef: GridColDef[]): CustomFooterType => { // Especificar el tipo de retorno
const getCellStyle = (colConfig: GridColDef | undefined, isPlaceholder: boolean = false) => {
@@ -303,11 +305,11 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
flex: colConfig.flex || undefined,
minWidth: colConfig.minWidth || colConfig.width || defaultWidth,
textAlign: (colConfig.align || 'right') as 'right' | 'left' | 'center',
pr: isPlaceholder || colConfig.field === columnsDef[columnsDef.length-1].field ? 0 : 1,
pr: isPlaceholder || colConfig.field === columnsDef[columnsDef.length - 1].field ? 0 : 1,
fontWeight: 'bold',
};
};
// eslint-disable-next-line react/display-name
const FooterComponent: CustomFooterType = (props) => ( // El componente debe aceptar props
<GridFooterContainer {...props} sx={{ // Pasar props y combinar sx
@@ -316,21 +318,21 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
borderTop: (theme) => `1px solid ${theme.palette.divider}`, minHeight: '52px',
}}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<GridFooter sx={{ borderTop: 'none', '& .MuiTablePagination-root, & .MuiDataGrid-selectedRowCount': { display: 'none' }}} />
<GridFooter sx={{ borderTop: 'none', '& .MuiTablePagination-root, & .MuiDataGrid-selectedRowCount': { display: 'none' } }} />
</Box>
<Box sx={{
p: theme => theme.spacing(0, 1), display: 'flex', alignItems: 'center',
<Box sx={{
p: theme => theme.spacing(0, 1), display: 'flex', alignItems: 'center',
fontWeight: 'bold', marginLeft: 'auto', whiteSpace: 'nowrap', overflowX: 'auto',
}}>
<Typography variant="subtitle2" sx={{ ...getCellStyle(columnsDef.find(c => c.field === 'publicacion' || c.field === columnsDef[0].field)), textAlign:'right' }}>TOTALES:</Typography>
<Typography variant="subtitle2" sx={{ ...getCellStyle(columnsDef.find(c => c.field === 'publicacion' || c.field === columnsDef[0].field)), textAlign: 'right' }}>TOTALES:</Typography>
<Typography variant="subtitle2" sx={{ ...getCellStyle(columnsDef.find(c => c.field === 'canilla' || c.field === 'tipoVendedor' || c.field === columnsDef[1].field), true) }}></Typography>
{columnsDef.some(c => c.field === 'fecha') &&
{columnsDef.some(c => c.field === 'fecha') &&
<Typography variant="subtitle2" sx={{ ...getCellStyle(columnsDef.find(c => c.field === 'fecha'), true) }}></Typography>
}
<Typography variant="subtitle2" sx={getCellStyle(columnsDef.find(c => c.field === 'totalCantSalida'))}>{numberFormatter(totals.totalCantSalida)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle(columnsDef.find(c => c.field === 'totalCantEntrada'))}>{numberFormatter(totals.totalCantEntrada)}</Typography>
<Typography variant="subtitle2" sx={getCellStyle(columnsDef.find(c => c.field === 'vendidos'))}>{numberFormatter(totals.vendidos)}</Typography>
<Typography variant="subtitle2" sx={{...getCellStyle(columnsDef.find(c => c.field === 'totalRendir')), pr:0 }}>{currencyFormatter(totals.totalRendir)}</Typography>
<Typography variant="subtitle2" sx={{ ...getCellStyle(columnsDef.find(c => c.field === 'totalRendir')), pr: 0 }}>{currencyFormatter(totals.totalRendir)}</Typography>
</Box>
</GridFooterContainer>
);
@@ -341,7 +343,7 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
const FooterAccionistas = useMemo(() => createCustomFooterComponent(totalesAccionistas, commonColumns), [totalesAccionistas]);
const FooterCanillasOtraFecha = useMemo(() => createCustomFooterComponent(totalesCanillasOtraFecha, commonColumnsWithFecha), [totalesCanillasOtraFecha]);
const FooterAccionistasOtraFecha = useMemo(() => createCustomFooterComponent(totalesAccionistasOtraFecha, commonColumnsWithFecha), [totalesAccionistasOtraFecha]);
const FooterResumen = useMemo(() => createCustomFooterComponent(totalesResumen, columnsTodos), [totalesResumen, columnsTodos]);
if (showParamSelector) {
return (
@@ -357,16 +359,16 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
</Box>
);
}
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 ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', {timeZone:'UTC'}) : ''}</Typography>
<Typography variant="h5">Reporte: Detalle Distribución Canillitas ({currentParams?.nombreEmpresa}) - {currentParams?.fecha ? new Date(currentParams.fecha + 'T00:00:00').toLocaleDateString('es-AR', { timeZone: 'UTC' }) : ''}</Typography>
<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">
<Button onClick={() => handleGenerarYAbrirPdf(true)} variant="contained" color="secondary" disabled={loadingPdf || !reportData || !!error} size="small">
{loadingPdf && pdfSoloTotales ? <CircularProgress size={20} color="inherit" /> : "PDF Totales"}
</Button>
<Button onClick={handleExportToExcel} variant="outlined" disabled={!reportData || !!error} size="small">
@@ -378,27 +380,33 @@ const ReporteDetalleDistribucionCanillasPage: React.FC = () => {
</Box>
</Box>
{loading && <Box sx={{ textAlign: 'center', my:2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{loading && <Box sx={{ textAlign: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="info" sx={{ my: 2 }}>{error}</Alert>}
{!loading && !error && reportData && (
<>
<DataGridSection title="Canillitas" data={reportData.canillas || []} columns={commonColumns} footerComponent={FooterCanillas} />
<DataGridSection title="Accionistas" data={reportData.canillasAccionistas || []} columns={commonColumns} footerComponent={FooterAccionistas} />
<DataGridSection title="Resumen por Tipo de Vendedor" data={reportData.canillasTodos || []} columns={columnsTodos} height={220}/>
<DataGridSection
title="Resumen por Tipo de Vendedor"
data={reportData.canillasTodos || []}
columns={columnsTodos}
footerComponent={FooterResumen} // <-- PASAR EL FOOTER
height={220} // El height ya no es necesario si autoHeight está activado por tener footer
/>
{reportData.canillasLiquidadasOtraFecha && reportData.canillasLiquidadasOtraFecha.length > 0 &&
<DataGridSection title="Canillitas (Liquidados de Otras Fechas)" data={reportData.canillasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterCanillasOtraFecha} />}
{reportData.canillasAccionistasLiquidadasOtraFecha && reportData.canillasAccionistasLiquidadasOtraFecha.length > 0 &&
<DataGridSection title="Accionistas (Liquidados de Otras Fechas)" data={reportData.canillasAccionistasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterAccionistasOtraFecha} />}
<DataGridSection title="Accionistas (Liquidados de Otras Fechas)" data={reportData.canillasAccionistasLiquidadasOtraFecha} columns={commonColumnsWithFecha} footerComponent={FooterAccionistasOtraFecha} />}
</>
)}
{!loading && !error && reportData &&
{!loading && !error && reportData &&
Object.values(reportData).every(arr => !arr || arr.length === 0) &&
<Typography sx={{mt: 2, fontStyle: 'italic'}}>No se encontraron datos para los criterios seleccionados.</Typography>
}
<Typography sx={{ mt: 2, fontStyle: 'italic' }}>No se encontraron datos para los criterios seleccionados.</Typography>
}
</Box>
);
};

View File

@@ -34,7 +34,7 @@ const SeleccionaReporteListadoDistribucionCanillas: React.FC<SeleccionaReporteLi
const fetchPublicaciones = async () => {
setLoadingDropdowns(true);
try {
const data = await publicacionService.getAllPublicaciones(undefined, undefined, true);
const data = await publicacionService.getAllPublicaciones(undefined, undefined);
setPublicaciones(data.map(p => p));
} catch (error) {
console.error("Error al cargar publicaciones:", error);

View File

@@ -38,7 +38,7 @@ const SeleccionaReporteListadoDistribucionCanillasImporte: React.FC<SeleccionaRe
const fetchPublicaciones = async () => {
setLoadingDropdowns(true);
try {
const data = await publicacionService.getAllPublicaciones(undefined, undefined, true);
const data = await publicacionService.getAllPublicaciones(undefined, undefined);
setPublicaciones(data.map(p => p));
} catch (error) {
console.error("Error al cargar publicaciones:", error);

View File

@@ -52,7 +52,7 @@ const getPublicacionesPorDiaSemana = async (diaSemana: number): Promise<Publicac
return response.data;
};
const getPublicacionesForDropdown = async (soloHabilitadas: boolean = true): Promise<PublicacionDropdownDto[]> => { // << NUEVA FUNCIÓN
const getPublicacionesForDropdown = async (soloHabilitadas: boolean): Promise<PublicacionDropdownDto[]> => {
const response = await apiClient.get<PublicacionDropdownDto[]>('/publicaciones/dropdown', { params: { soloHabilitadas } });
return response.data;
};