Refinamiento de permisos y ajustes en controles. Añade gestión sobre saldos y visualización. Entre otros..

This commit is contained in:
2025-06-06 18:33:09 -03:00
parent 8fb94f8cef
commit 35e24ab7d2
104 changed files with 5917 additions and 1205 deletions

View File

@@ -3,7 +3,8 @@ import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select, Checkbox, Tooltip,
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle
Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle,
ToggleButtonGroup, ToggleButton
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import PrintIcon from '@mui/icons-material/Print';
@@ -19,7 +20,7 @@ import canillaService from '../../services/Distribucion/canillaService';
import type { EntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/EntradaSalidaCanillaDto';
import type { UpdateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaCanillaDto';
import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto';
import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto';
import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
@@ -28,25 +29,32 @@ import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
import reportesService from '../../services/Reportes/reportesService';
type TipoDestinatarioFiltro = 'canillitas' | 'accionistas';
const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const [movimientos, setMovimientos] = useState<EntradaSalidaCanillaDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
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 [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroFecha, setFiltroFecha] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>('');
const [filtroIdCanilla, setFiltroIdCanilla] = useState<number | string>('');
const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados');
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
const [filtroIdCanillitaSeleccionado, setFiltroIdCanillitaSeleccionado] = useState<number | string>('');
const [filtroTipoDestinatario, setFiltroTipoDestinatario] = useState<TipoDestinatarioFiltro>('canillitas');
const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
const [loadingTicketPdf, setLoadingTicketPdf] = useState(false);
const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]);
const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null);
const [prefillModalData, setPrefillModalData] = useState<{
fecha?: string;
idCanilla?: number | string;
nombreCanilla?: string; // << AÑADIDO PARA PASAR AL MODAL
idPublicacion?: number | string;
} | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(25);
@@ -64,70 +72,123 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const puedeLiquidar = isSuperAdmin || tienePermiso("MC005");
const puedeEliminarLiquidados = isSuperAdmin || tienePermiso("MC006");
// Función para formatear fechas YYYY-MM-DD a DD/MM/YYYY
const formatDate = (dateString?: string | null): string => {
if (!dateString) return '-';
const datePart = dateString.split('T')[0];
const parts = datePart.split('-');
if (parts.length === 3) {
return `${parts[2]}/${parts[1]}/${parts[0]}`;
}
if (parts.length === 3) { return `${parts[2]}/${parts[1]}/${parts[0]}`; }
return datePart;
};
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const [pubsData, canData] = await Promise.all([
publicacionService.getAllPublicaciones(undefined, undefined, true),
canillaService.getAllCanillas(undefined, undefined, true)
]);
setPublicaciones(pubsData);
setCanillitas(canData);
} catch (err) {
console.error(err); setError("Error al cargar opciones de filtro.");
} finally { setLoadingFiltersDropdown(false); }
useEffect(() => {
const fetchPublicaciones = async () => {
setLoadingFiltersDropdown(true); // Mover al inicio de la carga de pubs
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
} finally {
// No poner setLoadingFiltersDropdown(false) aquí, esperar a que ambas cargas terminen
}
};
fetchPublicaciones();
}, []);
useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
const fetchDestinatariosParaDropdown = useCallback(async () => {
setLoadingFiltersDropdown(true); // Poner al inicio de esta carga también
setFiltroIdCanillitaSeleccionado('');
setDestinatariosDropdown([]);
setError(null); // Limpiar errores de carga de dropdowns previos
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
} finally {
setLoadingFiltersDropdown(false); // Poner al final de AMBAS cargas de dropdown
}
}, [filtroTipoDestinatario]);
useEffect(() => {
fetchDestinatariosParaDropdown();
}, [fetchDestinatariosParaDropdown]);
const cargarMovimientos = useCallback(async () => {
if (!puedeVer) { setError("No tiene permiso."); setLoading(false); return; }
if (!puedeVer) { setError("No tiene permiso para ver esta sección."); setLoading(false); return; }
if (!filtroFecha || !filtroIdCanillitaSeleccionado) {
if (loading) setLoading(false);
setMovimientos([]);
return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
let liquidadosFilter: boolean | null = null;
let incluirNoLiquidadosFilter: boolean | null = true; // Por defecto mostrar no liquidados
if (filtroEstadoLiquidacion === 'liquidados') {
liquidadosFilter = true;
incluirNoLiquidadosFilter = false;
} else if (filtroEstadoLiquidacion === 'noLiquidados') {
liquidadosFilter = false;
incluirNoLiquidadosFilter = true;
} // Si es 'todos', ambos son null o true y false respectivamente (backend debe manejarlo)
const params = {
fechaDesde: filtroFechaDesde || null, fechaHasta: filtroFechaHasta || null,
fechaDesde: filtroFecha,
fechaHasta: filtroFecha,
idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
idCanilla: filtroIdCanilla ? Number(filtroIdCanilla) : null,
liquidados: liquidadosFilter,
incluirNoLiquidados: filtroEstadoLiquidacion === 'todos' ? null : incluirNoLiquidadosFilter,
idCanilla: Number(filtroIdCanillitaSeleccionado),
liquidados: null,
incluirNoLiquidados: null,
};
const data = await entradaSalidaCanillaService.getAllEntradasSalidasCanilla(params);
setMovimientos(data);
setSelectedIdsParaLiquidar(new Set()); // Limpiar selección al recargar
setSelectedIdsParaLiquidar(new Set());
} catch (err) {
console.error(err); setError('Error al cargar movimientos.');
} finally { setLoading(false); }
}, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdCanilla, filtroEstadoLiquidacion]);
console.error("Error al cargar movimientos:", err);
setError('Error al cargar movimientos.');
setMovimientos([]);
} finally {
setLoading(false);
}
}, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdCanillitaSeleccionado]);
useEffect(() => {
if (filtroFecha && filtroIdCanillitaSeleccionado) {
cargarMovimientos();
} else {
setMovimientos([]);
if (loading) setLoading(false); // Asegurar que no se quede en loading si los filtros se limpian
}
}, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]); // `cargarMovimientos` ya tiene sus dependencias
useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]);
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
if (!puedeCrear && !item) {
setApiErrorMessage("No tiene permiso para registrar nuevos movimientos.");
return;
}
if (item && !puedeModificar) {
setApiErrorMessage("No tiene permiso para modificar movimientos.");
return;
}
if (item) {
setEditingMovimiento(item);
setPrefillModalData(null);
} else {
// --- CAMBIO: Obtener nombre del canillita seleccionado para prefill ---
const canillitaSeleccionado = destinatariosDropdown.find(
c => c.idCanilla === Number(filtroIdCanillitaSeleccionado)
);
setEditingMovimiento(null);
setPrefillModalData({
fecha: filtroFecha,
idCanilla: filtroIdCanillitaSeleccionado,
nombreCanilla: canillitaSeleccionado?.nomApe, // << AÑADIR NOMBRE
idPublicacion: filtroIdPublicacion
});
}
setApiErrorMessage(null);
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);
@@ -138,7 +199,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => {
// Almacenar el idParte en el propio elemento del menú para referencia
event.currentTarget.setAttribute('data-rowid', item.idParte.toString());
setAnchorEl(event.currentTarget);
setSelectedRow(item);
@@ -154,7 +214,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
});
};
const handleSelectAllForLiquidar = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
if (event.target.checked) {
const newSelectedIds = new Set(movimientos.filter(m => !m.liquidado).map(m => m.idParte));
setSelectedIdsParaLiquidar(newSelectedIds);
} else {
@@ -170,74 +230,65 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
setOpenLiquidarDialog(true);
};
const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false);
const handleConfirmLiquidar = async () => {
if (selectedIdsParaLiquidar.size === 0) {
setApiErrorMessage("No hay movimientos seleccionados para liquidar.");
return;
}
if (!fechaLiquidacionDialog) {
setApiErrorMessage("Debe seleccionar una fecha de liquidación.");
return;
}
// --- VALIDACIÓN DE FECHA ---
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z'); // Usar Z para consistencia con formatDate si es necesario, o T00:00:00 para local
if (selectedIdsParaLiquidar.size === 0) { /* ... */ return; }
if (!fechaLiquidacionDialog) { /* ... */ return; }
// ... (validación de fecha sin cambios)
const fechaLiquidacionDate = new Date(fechaLiquidacionDialog + 'T00:00:00Z');
let fechaMovimientoMasReciente: Date | null = null;
selectedIdsParaLiquidar.forEach(idParte => {
const movimiento = movimientos.find(m => m.idParte === idParte);
if (movimiento && movimiento.fecha) { // Asegurarse que movimiento.fecha existe
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z'); // Consistencia con Z
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime()
if (movimiento && movimiento.fecha) {
const movFecha = new Date(movimiento.fecha.split('T')[0] + 'T00:00:00Z');
if (fechaMovimientoMasReciente === null || movFecha.getTime() > (fechaMovimientoMasReciente as Date).getTime()) {
fechaMovimientoMasReciente = movFecha;
}
}
});
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando getTime()
if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) {
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); // Usar el loading general para la operación de liquidar
setLoading(true);
const liquidarDto: LiquidarMovimientosCanillaRequestDto = {
idsPartesALiquidar: Array.from(selectedIdsParaLiquidar),
fechaLiquidacion: fechaLiquidacionDialog // El backend espera YYYY-MM-DD
fechaLiquidacion: fechaLiquidacionDialog
};
try {
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
setOpenLiquidarDialog(false);
setOpenLiquidarDialog(false);
const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado);
await cargarMovimientos();
await cargarMovimientos();
if (movimientoParaTicket) {
console.log("Liquidación exitosa, intentando generar ticket para canillita:", movimientoParaTicket.idCanilla);
// --- CAMBIO: NO IMPRIMIR TICKET SI ES ACCIONISTA ---
if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) {
console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla);
await handleImprimirTicketLiquidacion(
movimientoParaTicket.idCanilla,
fechaLiquidacionDialog,
movimientoParaTicket.canillaEsAccionista
fechaLiquidacionDialog,
false // esAccionista = false
);
} else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) {
console.log("Liquidación exitosa para accionista. No se genera ticket automáticamente.");
} else {
console.warn("No se pudo encontrar información del movimiento para generar el ticket post-liquidación.");
console.warn("No se pudo encontrar información del movimiento para ticket post-liquidación.");
}
} catch (err: any) {
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al liquidar.';
setApiErrorMessage(msg);
setApiErrorMessage(msg);
} finally {
setLoading(false);
}
};
// Esta función se pasa al modal para que la invoque al hacer submit en MODO EDICIÓN
const handleModalEditSubmit = async (data: UpdateEntradaSalidaCanillaDto, idParte: number) => {
// ... (sin cambios)
setApiErrorMessage(null);
try {
await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data);
@@ -251,32 +302,21 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const handleCloseModal = () => {
setModalOpen(false);
setEditingMovimiento(null);
// Recargar siempre que se cierre el modal y no haya un error pendiente a nivel de página
// Opcionalmente, podrías tener una bandera ' cambiosGuardados' que el modal active
// para ser más selectivo con la recarga.
setPrefillModalData(null);
if (!apiErrorMessage) {
cargarMovimientos();
}
};
const handleImprimirTicketLiquidacion = useCallback(async (
// Parámetros necesarios para el ticket
idCanilla: number,
fecha: string, // Fecha para la que se genera el ticket (probablemente fechaLiquidacionDialog)
esAccionista: boolean
idCanilla: number, fecha: string, esAccionista: boolean
) => {
// ... (sin cambios)
setLoadingTicketPdf(true);
setApiErrorMessage(null);
try {
const params = {
fecha: fecha.split('T')[0], // Asegurar formato YYYY-MM-DD
idCanilla: idCanilla,
esAccionista: esAccionista,
};
const params = { fecha: fecha.split('T')[0], idCanilla, esAccionista };
const blob = await reportesService.getTicketLiquidacionCanillaPdf(params);
if (blob.type === "application/json") {
const text = await blob.text();
const msg = JSON.parse(text).message ?? "Error inesperado al generar el ticket PDF.";
@@ -287,16 +327,11 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
if (!w) alert("Permita popups para ver el PDF del ticket.");
}
} catch (error: any) {
console.error("Error al generar ticket de liquidación:", error);
const message = axios.isAxiosError(error) && error.response?.data?.message
? error.response.data.message
: 'Ocurrió un error al generar el ticket.';
console.error("Error al generar ticket:", error);
const message = axios.isAxiosError(error) && error.response?.data?.message ? error.response.data.message : 'Error al generar ticket.';
setApiErrorMessage(message);
} finally {
setLoadingTicketPdf(false);
// No cerramos el menú aquí si se llama desde handleConfirmLiquidar
}
}, []); // Dependencias vacías si no usa nada del scope exterior que cambie, o añadir si es necesario
} finally { setLoadingTicketPdf(false); }
}, []);
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
@@ -305,47 +340,77 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
};
const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
if (!loading && !puedeVer && !loadingFiltersDropdown && movimientos.length === 0 && !filtroFecha && !filtroIdCanillitaSeleccionado ) { // Modificado para solo mostrar si no hay filtros y no puede ver
return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
}
const numSelectedToLiquidate = selectedIdsParaLiquidar.size;
// Corregido: numNotLiquidatedOnPage debe calcularse sobre 'movimientos' filtrados, no solo 'displayData'
// O, si la selección es solo por página, displayData está bien. Asumamos selección por página por ahora.
const numNotLiquidatedOnPage = displayData.filter(m => !m.liquidado).length;
return (
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Entradas/Salidas Canillitas</Typography>
<Typography variant="h5" gutterBottom>Entradas/Salidas Canillitas & Accionistas</Typography>
<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 Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} />
<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
helperText={!filtroFecha ? "Fecha es obligatoria" : ""}
/>
<ToggleButtonGroup
color="primary"
value={filtroTipoDestinatario}
exclusive
onChange={(_, newValue: TipoDestinatarioFiltro | null) => {
if (newValue !== null) {
setFiltroTipoDestinatario(newValue);
}
}}
aria-label="Tipo de Destinatario"
size="small"
>
<ToggleButton value="canillitas">Canillitas</ToggleButton>
<ToggleButton value="accionistas">Accionistas</ToggleButton>
</ToggleButtonGroup>
<FormControl size="small" sx={{ minWidth: 220, flexGrow: 1 }} disabled={loadingFiltersDropdown} required error={!filtroIdCanillitaSeleccionado}>
<InputLabel>{filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}</InputLabel>
<Select
value={filtroIdCanillitaSeleccionado}
label={filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}
onChange={(e) => setFiltroIdCanillitaSeleccionado(e.target.value as number | string)}
>
<MenuItem value=""><em>Seleccione uno</em></MenuItem>
{destinatariosDropdown.map(c => <MenuItem key={c.idCanilla} value={c.idCanilla}>{c.nomApe} {c.legajo ? `(Leg: ${c.legajo})`: ''}</MenuItem>)}
</Select>
{!filtroIdCanillitaSeleccionado && <Typography component="p" color="error" variant="caption" sx={{ml:1.5, fontSize:'0.65rem'}}>Selección obligatoria</Typography>}
</FormControl>
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
<InputLabel>Publicación</InputLabel>
<Select value={filtroIdPublicacion} label="Publicación" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}>
<InputLabel>Publicación (Opcional)</InputLabel>
<Select value={filtroIdPublicacion} label="Publicación (Opcional)" onChange={(e) => setFiltroIdPublicacion(e.target.value as number | string)}>
<MenuItem value=""><em>Todas</em></MenuItem>
{publicaciones.map(p => <MenuItem key={p.idPublicacion} value={p.idPublicacion}>{p.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 200, flexGrow: 1 }} disabled={loadingFiltersDropdown}>
<InputLabel>Canillita</InputLabel>
<Select value={filtroIdCanilla} label="Canillita" onChange={(e) => setFiltroIdCanilla(e.target.value as number | string)}>
<MenuItem value=""><em>Todos</em></MenuItem>
{canillitas.map(c => <MenuItem key={c.idCanilla} value={c.idCanilla}>{c.nomApe}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
<InputLabel>Estado Liquidación</InputLabel>
<Select value={filtroEstadoLiquidacion} label="Estado Liquidación" onChange={(e) => setFiltroEstadoLiquidacion(e.target.value as 'todos' | 'liquidados' | 'noLiquidados')}>
<MenuItem value="noLiquidados">No Liquidados</MenuItem>
<MenuItem value="liquidados">Liquidados</MenuItem>
<MenuItem value="todos">Todos</MenuItem>
</Select>
</FormControl>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Movimiento</Button>)}
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && numSelectedToLiquidate > 0 && (
{/* --- CAMBIO: DESHABILITAR BOTÓN SI FILTROS OBLIGATORIOS NO ESTÁN --- */}
{puedeCrear && (
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => handleOpenModal()}
disabled={!filtroFecha || !filtroIdCanillitaSeleccionado} // <<-- AÑADIDO
>
Registrar Movimiento
</Button>
)}
{puedeLiquidar && numSelectedToLiquidate > 0 && movimientos.some(m => selectedIdsParaLiquidar.has(m.idParte) && !m.liquidado) && (
<Button variant="contained" color="success" startIcon={<PlaylistAddCheckIcon />} onClick={handleOpenLiquidarDialog}>
Liquidar Seleccionados ({numSelectedToLiquidate})
</Button>
@@ -353,8 +418,12 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
</Box>
</Paper>
{!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>}
{error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{/* 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>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{loadingTicketPdf &&
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}>
@@ -364,12 +433,13 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
{!loading && !error && puedeVer && filtroFecha && filtroIdCanillitaSeleccionado && (
// ... (Tabla y Paginación sin cambios)
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
{puedeLiquidar && (
<TableCell padding="checkbox">
<Checkbox
indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage && numNotLiquidatedOnPage > 0}
@@ -381,7 +451,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
)}
<TableCell>Fecha</TableCell>
<TableCell>Publicación</TableCell>
<TableCell>Canillita</TableCell>
<TableCell>{filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'}</TableCell>
<TableCell align="right">Salida</TableCell>
<TableCell align="right">Entrada</TableCell>
<TableCell align="right">Vendidos</TableCell>
@@ -397,19 +467,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
<TableRow>
<TableCell
colSpan={
(puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 1 : 0) +
9 +
((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0)
(puedeLiquidar ? 1 : 0) + 9 + ((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0)
}
align="center"
>
No se encontraron movimientos.
No se encontraron movimientos con los filtros aplicados.
</TableCell>
</TableRow>
) : (
displayData.map((m) => (
<TableRow key={m.idParte} hover selected={selectedIdsParaLiquidar.has(m.idParte)}>
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
{puedeLiquidar && (
<TableCell padding="checkbox">
<Checkbox
checked={selectedIdsParaLiquidar.has(m.idParte)}
@@ -440,8 +508,8 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
<TableCell align="right">
<IconButton
onClick={(e) => handleMenuOpen(e, m)}
data-rowid={m.idParte.toString()} // Guardar el id de la fila aquí
disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar} // Lógica simplificada, refinar si es necesario
data-rowid={m.idParte.toString()}
disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar}
>
<MoreVertIcon />
</IconButton>
@@ -462,19 +530,17 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
<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>)}
{/* Opción de Imprimir Ticket Liq. */}
{selectedRow && selectedRow.liquidado && ( // Solo mostrar si ya está liquidado (para reimprimir)
{/* --- CAMBIO: MOSTRAR REIMPRIMIR TICKET SIEMPRE SI ESTÁ LIQUIDADO --- */}
{selectedRow && selectedRow.liquidado && puedeLiquidar && ( // Usar puedeLiquidar para consistencia
<MenuItem
onClick={() => {
if (selectedRow) { // selectedRow no será null aquí debido a la condición anterior
if (selectedRow) {
handleImprimirTicketLiquidacion(
selectedRow.idCanilla,
selectedRow.fechaLiquidado || selectedRow.fecha, // Usar fechaLiquidado si existe, sino la fecha del movimiento
selectedRow.canillaEsAccionista
selectedRow.fechaLiquidado || selectedRow.fecha,
selectedRow.canillaEsAccionista // Pasar si es accionista
);
}
// handleMenuClose() es llamado por handleImprimirTicketLiquidacion
}}
disabled={loadingTicketPdf}
>
@@ -483,13 +549,10 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
Reimprimir Ticket Liq.
</MenuItem>
)}
{selectedRow && ( // Opción de Eliminar
{selectedRow && (
((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados))
) && (
<MenuItem onClick={() => {
if (selectedRow) handleDelete(selectedRow.idParte);
}}>
<MenuItem onClick={() => { if (selectedRow) handleDelete(selectedRow.idParte); }}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
</MenuItem>
)}
@@ -498,13 +561,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
<EntradaSalidaCanillaFormModal
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleModalEditSubmit}
onSubmit={handleModalEditSubmit} // Este onSubmit es solo para edición
initialData={editingMovimiento}
prefillData={prefillModalData}
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
{/* ... (Dialog de Liquidación sin cambios) ... */}
<Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
<DialogTitle>Confirmar Liquidación</DialogTitle>
<DialogContent>
<DialogContentText>
@@ -523,7 +588,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
</Button>
</DialogActions>
</Dialog>
</Box>
);
};