Fix Selectores de Fechas Reporte Existencia de Papel.

Se agrega Total a Liquidar para la E/S de Canillitas.
This commit is contained in:
2025-06-13 13:23:05 -03:00
parent b04a3b99bf
commit cec471b4b1
6 changed files with 106 additions and 72 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react'; // << Añadido useMemo
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
@@ -33,9 +33,9 @@ type TipoDestinatarioFiltro = 'canillitas' | 'accionistas';
const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const [movimientos, setMovimientos] = useState<EntradaSalidaCanillaDto[]>([]);
const [loading, setLoading] = useState(true); // Para carga principal de movimientos
const [error, setError] = useState<string | null>(null); // Error general o de carga
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); // Para errores de modal/API
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [filtroFecha, setFiltroFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
@@ -52,7 +52,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const [prefillModalData, setPrefillModalData] = useState<{
fecha?: string;
idCanilla?: number | string;
nombreCanilla?: string; // << AÑADIDO PARA PASAR AL MODAL
nombreCanilla?: string;
idPublicacion?: number | string;
} | null>(null);
@@ -82,34 +82,34 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
useEffect(() => {
const fetchPublicaciones = async () => {
setLoadingFiltersDropdown(true); // Mover al inicio de la carga de pubs
setLoadingFiltersDropdown(true);
try {
const pubsData = await publicacionService.getPublicacionesForDropdown(true);
setPublicaciones(pubsData);
} catch (err) {
console.error("Error cargando publicaciones para filtro:",err);
setError("Error al cargar publicaciones."); // Usar error general
setError("Error al cargar publicaciones.");
} finally {
// No poner setLoadingFiltersDropdown(false) aquí, esperar a que ambas cargas terminen
// No setLoadingFiltersDropdown(false) a, esperar a la otra carga
}
};
fetchPublicaciones();
}, []);
const fetchDestinatariosParaDropdown = useCallback(async () => {
setLoadingFiltersDropdown(true); // Poner al inicio de esta carga también
setLoadingFiltersDropdown(true);
setFiltroIdCanillitaSeleccionado('');
setDestinatariosDropdown([]);
setError(null); // Limpiar errores de carga de dropdowns previos
setError(null);
try {
const esAccionistaFilter = filtroTipoDestinatario === 'accionistas';
const data = await canillaService.getAllCanillas(undefined, undefined, true, esAccionistaFilter);
setDestinatariosDropdown(data);
} catch (err) {
console.error("Error cargando destinatarios para filtro:", err);
setError("Error al cargar canillitas/accionistas."); // Usar error general
setError("Error al cargar canillitas/accionistas.");
} finally {
setLoadingFiltersDropdown(false); // Poner al final de AMBAS cargas de dropdown
setLoadingFiltersDropdown(false);
}
}, [filtroTipoDestinatario]);
@@ -153,9 +153,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
cargarMovimientos();
} else {
setMovimientos([]);
if (loading) setLoading(false); // Asegurar que no se quede en loading si los filtros se limpian
if (loading) setLoading(false);
}
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]); // `cargarMovimientos` ya tiene sus dependencias
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]);
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
@@ -172,7 +172,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
setEditingMovimiento(item);
setPrefillModalData(null);
} else {
// --- CAMBIO: Obtener nombre del canillita seleccionado para prefill ---
const canillitaSeleccionado = destinatariosDropdown.find(
c => c.idCanilla === Number(filtroIdCanillitaSeleccionado)
);
@@ -180,7 +179,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
setPrefillModalData({
fecha: filtroFecha,
idCanilla: filtroIdCanillitaSeleccionado,
nombreCanilla: canillitaSeleccionado?.nomApe, // << AÑADIR NOMBRE
nombreCanilla: canillitaSeleccionado?.nomApe,
idPublicacion: filtroIdPublicacion
});
}
@@ -188,7 +187,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
setModalOpen(true);
};
// ... handleDelete, handleMenuOpen, handleMenuClose, handleSelectRowForLiquidar, handleSelectAllForLiquidar, handleOpenLiquidarDialog, handleCloseLiquidarDialog sin cambios ...
const handleDelete = async (idParte: number) => {
if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) {
setApiErrorMessage(null);
@@ -233,9 +231,8 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const handleConfirmLiquidar = async () => {
if (selectedIdsParaLiquidar.size === 0) { /* ... */ return; }
if (!fechaLiquidacionDialog) { /* ... */ return; }
// ... (validación de fecha sin cambios)
if (selectedIdsParaLiquidar.size === 0) { return; }
if (!fechaLiquidacionDialog) { return; }
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z');
let fechaMovimientoMasReciente: Date | null = null;
selectedIdsParaLiquidar.forEach(idParte => {
@@ -251,7 +248,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
setApiErrorMessage(`La fecha de liquidación (${fechaLiquidacionDate.toLocaleDateString('es-AR', {timeZone: 'UTC'})}) no puede ser inferior a la fecha del movimiento más reciente a liquidar (${(fechaMovimientoMasReciente as Date).toLocaleDateString('es-AR', {timeZone: 'UTC'})}).`);
return;
}
setApiErrorMessage(null);
setLoading(true);
const liquidarDto: LiquidarMovimientosCanillaRequestDto = {
@@ -262,17 +258,18 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
setOpenLiquidarDialog(false);
const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
// Necesitamos encontrar el movimiento en la lista ANTES de recargar
const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado);
await cargarMovimientos();
await cargarMovimientos(); // Recargar la lista para reflejar el estado liquidado
// --- CAMBIO: NO IMPRIMIR TICKET SI ES ACCIONISTA ---
// Usar la fecha del movimiento original para el ticket
if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) {
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
await handleImprimirTicketLiquidacion(
movimientoParaTicket.idCanilla,
fechaLiquidacionDialog,
false // esAccionista = false
movimientoParaTicket.fecha, // Usar la fecha del movimiento
false
);
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
console.log("Liquidación exitosa para accionista. No se genera ticket automáticamente.");
@@ -288,7 +285,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
};
const handleModalEditSubmit = async (data: UpdateEntradaSalidaCanillaDto, idParte: number) => {
// ... (sin cambios)
setApiErrorMessage(null);
try {
await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data);
@@ -311,7 +307,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const handleImprimirTicketLiquidacion = useCallback(async (
idCanilla: number, fecha: string, esAccionista: boolean
) => {
// ... (sin cambios)
setLoadingTicketPdf(true);
setApiErrorMessage(null);
try {
@@ -340,11 +335,14 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
};
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeVer && !loadingFiltersDropdown && movimientos.length === 0 && !filtroFecha && !filtroIdCanillitaSeleccionado ) { // Modificado para solo mostrar si no hay filtros y no puede ver
const totalARendirVisible = useMemo(() =>
displayData.filter(m => !m.liquidado).reduce((sum, item) => sum + item.montoARendir, 0)
, [displayData]);
if (!loading && !puedeVer && !loadingFiltersDropdown && movimientos.length === 0 && !filtroFecha && !filtroIdCanillitaSeleccionado ) {
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
}
const numSelectedToLiquidate = selectedIdsParaLiquidar.size;
const numNotLiquidatedOnPage = displayData.filter(m => !m.liquidado).length;
@@ -354,11 +352,12 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>Filtros <FilterListIcon fontSize="small" /></Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center', mb: 2 }}>
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
{/* ... (Filtros sin cambios) ... */}
<TextField label="Fecha" type="date" size="small" value={filtroFecha}
onChange={(e) => setFiltroFecha(e.target.value)}
InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }}
required
error={!filtroFecha} // Se marca error si está vacío
error={!filtroFecha}
helperText={!filtroFecha ? "Fecha es obligatoria" : ""}
/>
<ToggleButtonGroup
@@ -398,14 +397,13 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
</Select>
</FormControl>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{/* --- CAMBIO: DESHABILITAR BOTÓN SI FILTROS OBLIGATORIOS NO ESTÁN --- */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap:2 }}>
{puedeCrear && (
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => handleOpenModal()}
disabled={!filtroFecha || !filtroIdCanillitaSeleccionado} // <<-- AÑADIDO
disabled={!filtroFecha || !filtroIdCanillitaSeleccionado}
>
Registrar Movimiento
</Button>
@@ -417,26 +415,32 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
)}
</Box>
</Paper>
{!filtroFecha && <Alert severity="info" sx={{my:1}}>Por favor, seleccione una fecha.</Alert>}
{!filtroFecha && <Alert severity="info" sx={{my:1}}>Por favor, seleccione una fecha.</Alert>}
{filtroFecha && !filtroIdCanillitaSeleccionado && <Alert severity="info" sx={{my:1}}>Por favor, seleccione un {filtroTipoDestinatario === 'canillitas' ? 'canillita' : 'accionista'}.</Alert>}
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{/* Mostrar error general si no hay error de API específico y no está cargando filtros */}
{error && !loading && !apiErrorMessage && !loadingFiltersDropdown && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{loadingTicketPdf &&
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}>
<CircularProgress size={20} sx={{ mr: 1 }} />
<Typography variant="body2">Cargando ticket...</Typography>
</Box>
}
{loadingTicketPdf && ( <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}> <CircularProgress size={20} sx={{ mr: 1 }} /> <Typography variant="body2">Cargando ticket...</Typography> </Box> )}
{!loading && movimientos.length > 0 && (
<Paper sx={{ p: 1.5, mb: 2, mt:1, backgroundColor: 'grey.100' }}>
<Box sx={{display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}>
<Typography variant="subtitle1" sx={{mr:2}}>
Total a Liquidar:
</Typography>
<Typography variant="h6" sx={{fontWeight: 'bold', color: 'error.main'}}>
{totalARendirVisible.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
</Typography>
</Box>
</Paper>
)}
{!loading && !error && puedeVer && filtroFecha && filtroIdCanillitaSeleccionado && (
// ... (Tabla y Paginación sin cambios)
<TableContainer component={Paper}>
<Table size="small">
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
{puedeLiquidar && (
@@ -527,20 +531,22 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeModificar && selectedRow && !selectedRow.liquidado && (
<MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)}
{/* --- CAMBIO: MOSTRAR REIMPRIMIR TICKET SIEMPRE SI ESTÁ LIQUIDADO --- */}
{selectedRow && selectedRow.liquidado && puedeLiquidar && ( // Usar puedeLiquidar para consistencia
{selectedRow && selectedRow.liquidado && puedeLiquidar && (
<MenuItem
onClick={() => {
if (selectedRow) {
// Usar siempre selectedRow.fecha, que es la fecha original del movimiento
handleImprimirTicketLiquidacion(
selectedRow.idCanilla,
selectedRow.fechaLiquidado || selectedRow.fecha,
selectedRow.canillaEsAccionista // Pasar si es accionista
selectedRow.fecha, // Usar siempre la fecha del movimiento
selectedRow.canillaEsAccionista
);
}
handleMenuClose();
}}
disabled={loadingTicketPdf}
>
@@ -552,8 +558,9 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
{selectedRow && (
((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados))
) && (
<MenuItem onClick={() => { if (selectedRow) handleDelete(selectedRow.idParte); }}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
<MenuItem onClick={() => {if (selectedRow) handleDelete(selectedRow.idParte);}}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
Eliminar
</MenuItem>
)}
</Menu>
@@ -561,14 +568,13 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
<EntradaSalidaCanillaFormModal
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleModalEditSubmit} // Este onSubmit es solo para edición
onSubmit={handleModalEditSubmit}
initialData={editingMovimiento}
prefillData={prefillModalData}
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
{/* ... (Dialog de Liquidación sin cambios) ... */}
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
<DialogTitle>Confirmar Liquidación</DialogTitle>
<DialogContent>

View File

@@ -1,4 +1,3 @@
// src/pages/Reportes/ReporteExistenciaPapelPage.tsx
import React, { useState, useCallback } from 'react';
import {
Box,

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import {
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, Checkbox, FormControlLabel
Box, Typography, TextField, Button, CircularProgress, Alert,
FormControl, InputLabel, Select, MenuItem, Checkbox, FormControlLabel
} from '@mui/material';
import type { PlantaDropdownDto } from '../../models/dtos/Impresion/PlantaDropdownDto';
import plantaService from '../../services/Impresion/plantaService';
@@ -12,10 +12,10 @@ interface SeleccionaReporteExistenciaPapelProps {
fechaHasta: string;
idPlanta?: number | null;
consolidado: boolean;
}) => Promise<void>; // La función que realmente llama al servicio y maneja los datos
onCancel: () => void; // Para cerrar el modal/componente
isLoading?: boolean; // Para mostrar estado de carga desde el padre
apiErrorMessage?: string | null; // Para mostrar errores de API desde el padre
}) => Promise<void>;
onCancel?: () => void; // onCancel sigue acá por si lo necesito en otros selectores
isLoading?: boolean;
apiErrorMessage?: string | null;
}
const SeleccionaReporteExistenciaPapel: React.FC<SeleccionaReporteExistenciaPapelProps> = ({
@@ -24,7 +24,12 @@ const SeleccionaReporteExistenciaPapel: React.FC<SeleccionaReporteExistenciaPape
apiErrorMessage
}) => {
const [fechaDesde, setFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [fechaHasta, setFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
const [fechaHasta, setFechaHasta] = useState<string>(() => {
// Inicializar fechaHasta al día siguiente por defecto
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
return tomorrow.toISOString().split('T')[0];
});
const [idPlanta, setIdPlanta] = useState<number | string>('');
const [consolidado, setConsolidado] = useState<boolean>(false);
@@ -49,18 +54,39 @@ const SeleccionaReporteExistenciaPapel: React.FC<SeleccionaReporteExistenciaPape
}, []);
useEffect(() => {
// Si se marca consolidado, limpiar y deshabilitar la selección de planta
if (consolidado) {
setIdPlanta('');
}
}, [consolidado]);
const handleFechaDesdeChange = (newFechaDesde: string) => {
setFechaDesde(newFechaDesde);
// Limpiar errores
setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null }));
// Si la nueva fechaDesde es igual o posterior a la fechaHasta, ajustar fechaHasta
if (newFechaDesde && fechaHasta && new Date(newFechaDesde) >= new Date(fechaHasta)) {
const dDesde = new Date(newFechaDesde + 'T00:00:00'); // Usar T00:00:00 para evitar problemas de zona horaria
dDesde.setDate(dDesde.getDate() + 1);
setFechaHasta(dDesde.toISOString().split('T')[0]);
}
};
// Función para obtener la fecha mínima permitida para fechaHasta
const getMinFechaHasta = () => {
if (!fechaDesde) return undefined;
const minDate = new Date(fechaDesde + 'T00:00:00');
minDate.setDate(minDate.getDate() + 1); // El mínimo es el día SIGUIENTE a fechaDesde
return minDate.toISOString().split('T')[0];
};
const validate = (): boolean => {
const errors: { [key: string]: string | null } = {};
if (!fechaDesde) errors.fechaDesde = 'Fecha Desde es obligatoria.';
if (!fechaHasta) errors.fechaHasta = 'Fecha Hasta es obligatoria.';
if (fechaDesde && fechaHasta && new Date(fechaDesde) > new Date(fechaHasta)) {
errors.fechaHasta = 'Fecha Hasta no puede ser anterior a Fecha Desde.';
if (fechaDesde && fechaHasta && new Date(fechaHasta) <= new Date(fechaDesde)) {
errors.fechaHasta = 'Fecha Hasta debe ser posterior a Fecha Desde.';
}
if (!consolidado && !idPlanta) {
errors.idPlanta = 'Seleccione una planta si no es consolidado.';
@@ -88,7 +114,7 @@ const SeleccionaReporteExistenciaPapel: React.FC<SeleccionaReporteExistenciaPape
label="Fecha Desde"
type="date"
value={fechaDesde}
onChange={(e) => { setFechaDesde(e.target.value); setLocalErrors(p => ({ ...p, fechaDesde: null, fechaHasta: null })); }}
onChange={(e) => handleFechaDesdeChange(e.target.value)}
margin="normal"
fullWidth
required
@@ -109,6 +135,9 @@ const SeleccionaReporteExistenciaPapel: React.FC<SeleccionaReporteExistenciaPape
helperText={localErrors.fechaHasta}
disabled={isLoading}
InputLabelProps={{ shrink: true }}
inputProps={{
min: getMinFechaHasta()
}}
/>
<FormControlLabel
control={
@@ -126,7 +155,7 @@ const SeleccionaReporteExistenciaPapel: React.FC<SeleccionaReporteExistenciaPape
<Select
labelId="planta-select-label"
label="Planta"
value={consolidado ? '' : idPlanta} // Limpiar selección si es consolidado
value={consolidado ? '' : idPlanta}
onChange={(e) => { setIdPlanta(e.target.value as number); setLocalErrors(p => ({ ...p, idPlanta: null })); }}
>
<MenuItem value="" disabled><em>{consolidado ? 'N/A (Consolidado)' : 'Seleccione una planta'}</em></MenuItem>