531 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			531 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import React, { useState, useEffect, useCallback } from 'react';
 | |
| 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
 | |
| } from '@mui/material';
 | |
| import AddIcon from '@mui/icons-material/Add';
 | |
| import PrintIcon from '@mui/icons-material/Print';
 | |
| import MoreVertIcon from '@mui/icons-material/MoreVert';
 | |
| import EditIcon from '@mui/icons-material/Edit';
 | |
| import DeleteIcon from '@mui/icons-material/Delete';
 | |
| import FilterListIcon from '@mui/icons-material/FilterList';
 | |
| import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck';
 | |
| 
 | |
| import entradaSalidaCanillaService from '../../services/Distribucion/entradaSalidaCanillaService';
 | |
| import publicacionService from '../../services/Distribucion/publicacionService';
 | |
| 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 { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto';
 | |
| import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto';
 | |
| 
 | |
| import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal';
 | |
| import { usePermissions } from '../../hooks/usePermissions';
 | |
| import axios from 'axios';
 | |
| import reportesService from '../../services/Reportes/reportesService';
 | |
| 
 | |
| 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 [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
 | |
|   const [filtroFechaHasta, setFiltroFechaHasta] = 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 [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]);
 | |
|   const [canillitas, setCanillitas] = useState<CanillaDto[]>([]);
 | |
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
 | |
| 
 | |
|   const [modalOpen, setModalOpen] = useState(false);
 | |
|   const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null);
 | |
| 
 | |
|   const [page, setPage] = useState(0);
 | |
|   const [rowsPerPage, setRowsPerPage] = useState(25);
 | |
|   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
 | |
|   const [selectedRow, setSelectedRow] = useState<EntradaSalidaCanillaDto | null>(null);
 | |
|   const [selectedIdsParaLiquidar, setSelectedIdsParaLiquidar] = useState<Set<number>>(new Set());
 | |
|   const [fechaLiquidacionDialog, setFechaLiquidacionDialog] = useState<string>(new Date().toISOString().split('T')[0]);
 | |
|   const [openLiquidarDialog, setOpenLiquidarDialog] = useState(false);
 | |
| 
 | |
|   const { tienePermiso, isSuperAdmin } = usePermissions();
 | |
|   const puedeVer = isSuperAdmin || tienePermiso("MC001");
 | |
|   const puedeCrear = isSuperAdmin || tienePermiso("MC002");
 | |
|   const puedeModificar = isSuperAdmin || tienePermiso("MC003");
 | |
|   const puedeEliminar = isSuperAdmin || tienePermiso("MC004");
 | |
|   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]}`;
 | |
|     }
 | |
|     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(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]);
 | |
| 
 | |
|   const cargarMovimientos = useCallback(async () => {
 | |
|     if (!puedeVer) { setError("No tiene permiso."); setLoading(false); 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,
 | |
|         idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null,
 | |
|         idCanilla: filtroIdCanilla ? Number(filtroIdCanilla) : null,
 | |
|         liquidados: liquidadosFilter,
 | |
|         incluirNoLiquidados: filtroEstadoLiquidacion === 'todos' ? null : incluirNoLiquidadosFilter,
 | |
|       };
 | |
|       const data = await entradaSalidaCanillaService.getAllEntradasSalidasCanilla(params);
 | |
|       setMovimientos(data);
 | |
|       setSelectedIdsParaLiquidar(new Set()); // Limpiar selección al recargar
 | |
|     } catch (err) {
 | |
|       console.error(err); setError('Error al cargar movimientos.');
 | |
|     } finally { setLoading(false); }
 | |
|   }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdPublicacion, filtroIdCanilla, filtroEstadoLiquidacion]);
 | |
| 
 | |
|   useEffect(() => { cargarMovimientos(); }, [cargarMovimientos]);
 | |
| 
 | |
|   const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
 | |
|     setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
 | |
|   };
 | |
| 
 | |
|   const handleDelete = async (idParte: number) => {
 | |
|     if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) {
 | |
|       setApiErrorMessage(null);
 | |
|       try { await entradaSalidaCanillaService.deleteEntradaSalidaCanilla(idParte); cargarMovimientos(); }
 | |
|       catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
 | |
|     }
 | |
|     handleMenuClose();
 | |
|   };
 | |
| 
 | |
|   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);
 | |
|   };
 | |
|   const handleMenuClose = () => { setAnchorEl(null); setSelectedRow(null); };
 | |
| 
 | |
|   const handleSelectRowForLiquidar = (idParte: number) => {
 | |
|     setSelectedIdsParaLiquidar(prev => {
 | |
|       const newSet = new Set(prev);
 | |
|       if (newSet.has(idParte)) newSet.delete(idParte);
 | |
|       else newSet.add(idParte);
 | |
|       return newSet;
 | |
|     });
 | |
|   };
 | |
|   const handleSelectAllForLiquidar = (event: React.ChangeEvent<HTMLInputElement>) => {
 | |
|     if (event.target.checked) {
 | |
|       const newSelectedIds = new Set(movimientos.filter(m => !m.liquidado).map(m => m.idParte));
 | |
|       setSelectedIdsParaLiquidar(newSelectedIds);
 | |
|     } else {
 | |
|       setSelectedIdsParaLiquidar(new Set());
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const handleOpenLiquidarDialog = () => {
 | |
|     if (selectedIdsParaLiquidar.size === 0) {
 | |
|       setApiErrorMessage("Seleccione al menos un movimiento para liquidar.");
 | |
|       return;
 | |
|     }
 | |
|     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
 | |
| 
 | |
|     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()
 | |
|                 fechaMovimientoMasReciente = movFecha;
 | |
|             }
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     if (fechaMovimientoMasReciente !== null && fechaLiquidacionDate.getTime() < (fechaMovimientoMasReciente as Date).getTime()) { // Comparar usando 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
 | |
| 
 | |
|     const liquidarDto: LiquidarMovimientosCanillaRequestDto = {
 | |
|       idsPartesALiquidar: Array.from(selectedIdsParaLiquidar),
 | |
|       fechaLiquidacion: fechaLiquidacionDialog // El backend espera YYYY-MM-DD
 | |
|     };
 | |
| 
 | |
|     try {
 | |
|       await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
 | |
|       setOpenLiquidarDialog(false); 
 | |
|       
 | |
|       const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0];
 | |
|       const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado);
 | |
| 
 | |
|       await cargarMovimientos(); 
 | |
| 
 | |
|       if (movimientoParaTicket) {
 | |
|         console.log("Liquidación exitosa, intentando generar ticket para canillita:", movimientoParaTicket.idCanilla);
 | |
|         await handleImprimirTicketLiquidacion(
 | |
|             movimientoParaTicket.idCanilla,
 | |
|             fechaLiquidacionDialog, 
 | |
|             movimientoParaTicket.canillaEsAccionista
 | |
|         );
 | |
|       } else {
 | |
|         console.warn("No se pudo encontrar información del movimiento para generar el 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); 
 | |
|     } 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) => {
 | |
|     setApiErrorMessage(null);
 | |
|     try {
 | |
|       await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data);
 | |
|     } catch (err: any) {
 | |
|       const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar los cambios.';
 | |
|       setApiErrorMessage(message);
 | |
|       throw err;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   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.
 | |
|     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
 | |
|   ) => {
 | |
|     setLoadingTicketPdf(true);
 | |
|     setApiErrorMessage(null);
 | |
| 
 | |
|     try {
 | |
|       const params = {
 | |
|         fecha: fecha.split('T')[0], // Asegurar formato YYYY-MM-DD
 | |
|         idCanilla: idCanilla,
 | |
|         esAccionista: 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.";
 | |
|         setApiErrorMessage(msg);
 | |
|       } else {
 | |
|         const url = URL.createObjectURL(blob);
 | |
|         const w = window.open(url, '_blank');
 | |
|         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.';
 | |
|       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
 | |
| 
 | |
| 
 | |
|   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
 | |
|   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
 | |
|     setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
 | |
|   };
 | |
|   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>;
 | |
| 
 | |
|   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>
 | |
|       <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 }} />
 | |
|           <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)}>
 | |
|               <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 && (
 | |
|             <Button variant="contained" color="success" startIcon={<PlaylistAddCheckIcon />} onClick={handleOpenLiquidarDialog}>
 | |
|               Liquidar Seleccionados ({numSelectedToLiquidate})
 | |
|             </Button>
 | |
|           )}
 | |
|         </Box>
 | |
|       </Paper>
 | |
| 
 | |
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
 | |
|       {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>
 | |
|       }
 | |
| 
 | |
| 
 | |
|       {!loading && !error && puedeVer && (
 | |
|         <TableContainer component={Paper}>
 | |
|           <Table size="small">
 | |
|             <TableHead>
 | |
|               <TableRow>
 | |
|                 {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
 | |
|                   <TableCell padding="checkbox">
 | |
|                     <Checkbox
 | |
|                       indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage && numNotLiquidatedOnPage > 0}
 | |
|                       checked={numNotLiquidatedOnPage > 0 && numSelectedToLiquidate === numNotLiquidatedOnPage}
 | |
|                       onChange={handleSelectAllForLiquidar}
 | |
|                       disabled={numNotLiquidatedOnPage === 0}
 | |
|                     />
 | |
|                   </TableCell>
 | |
|                 )}
 | |
|                 <TableCell>Fecha</TableCell>
 | |
|                 <TableCell>Publicación</TableCell>
 | |
|                 <TableCell>Canillita</TableCell>
 | |
|                 <TableCell align="right">Salida</TableCell>
 | |
|                 <TableCell align="right">Entrada</TableCell>
 | |
|                 <TableCell align="right">Vendidos</TableCell>
 | |
|                 <TableCell align="right">A Rendir</TableCell>
 | |
|                 <TableCell>Liquidado</TableCell>
 | |
|                 <TableCell>F. Liq.</TableCell>
 | |
|                 <TableCell>Obs.</TableCell>
 | |
|                 {(puedeModificar || puedeEliminar || puedeLiquidar) && <TableCell align="right">Acciones</TableCell>}
 | |
|               </TableRow>
 | |
|             </TableHead>
 | |
|             <TableBody>
 | |
|               {displayData.length === 0 ? (
 | |
|                 <TableRow>
 | |
|                   <TableCell
 | |
|                     colSpan={
 | |
|                       (puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' ? 1 : 0) +
 | |
|                       9 +
 | |
|                       ((puedeModificar || puedeEliminar || puedeLiquidar) ? 1 : 0)
 | |
|                     }
 | |
|                     align="center"
 | |
|                   >
 | |
|                     No se encontraron movimientos.
 | |
|                   </TableCell>
 | |
|                 </TableRow>
 | |
|               ) : (
 | |
|                 displayData.map((m) => (
 | |
|                   <TableRow key={m.idParte} hover selected={selectedIdsParaLiquidar.has(m.idParte)}>
 | |
|                     {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
 | |
|                       <TableCell padding="checkbox">
 | |
|                         <Checkbox
 | |
|                           checked={selectedIdsParaLiquidar.has(m.idParte)}
 | |
|                           onChange={() => handleSelectRowForLiquidar(m.idParte)}
 | |
|                           disabled={m.liquidado}
 | |
|                         />
 | |
|                       </TableCell>
 | |
|                     )}
 | |
|                     <TableCell>{formatDate(m.fecha)}</TableCell>
 | |
|                     <TableCell>{m.nombrePublicacion}</TableCell>
 | |
|                     <TableCell>{m.nomApeCanilla}</TableCell>
 | |
|                     <TableCell align="right">{m.cantSalida}</TableCell>
 | |
|                     <TableCell align="right">{m.cantEntrada}</TableCell>
 | |
|                     <TableCell align="right" sx={{ fontWeight: 'bold' }}>{m.vendidos}</TableCell>
 | |
|                     <TableCell align="right" sx={{ fontWeight: 'bold' }}>
 | |
|                       {m.montoARendir.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })}
 | |
|                     </TableCell>
 | |
|                     <TableCell align="center">{m.liquidado ? <Chip label="Sí" color="success" size="small" /> : <Chip label="No" size="small" />}</TableCell>
 | |
|                     <TableCell>{m.fechaLiquidado ? formatDate(m.fechaLiquidado) : '-'}</TableCell>
 | |
|                     <TableCell>
 | |
|                       <Tooltip title={m.observacion || ''}>
 | |
|                         <Box sx={{ maxWidth: 100, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
 | |
|                           {m.observacion || '-'}
 | |
|                         </Box>
 | |
|                       </Tooltip>
 | |
|                     </TableCell>
 | |
|                     {(puedeModificar || puedeEliminar || puedeLiquidar) && (
 | |
|                       <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
 | |
|                         >
 | |
|                           <MoreVertIcon />
 | |
|                         </IconButton>
 | |
|                       </TableCell>
 | |
|                     )}
 | |
|                   </TableRow>
 | |
|                 )))}
 | |
|             </TableBody>
 | |
|           </Table>
 | |
|           <TablePagination
 | |
|             rowsPerPageOptions={[25, 50, 100]} component="div" count={movimientos.length}
 | |
|             rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
 | |
|             onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
 | |
|           />
 | |
|         </TableContainer>
 | |
|       )}
 | |
| 
 | |
|       <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)
 | |
|           <MenuItem
 | |
|             onClick={() => {
 | |
|               if (selectedRow) { // selectedRow no será null aquí debido a la condición anterior
 | |
|                 handleImprimirTicketLiquidacion(
 | |
|                   selectedRow.idCanilla,
 | |
|                   selectedRow.fechaLiquidado || selectedRow.fecha, // Usar fechaLiquidado si existe, sino la fecha del movimiento
 | |
|                   selectedRow.canillaEsAccionista
 | |
|                 );
 | |
|               }
 | |
|               // handleMenuClose() es llamado por handleImprimirTicketLiquidacion
 | |
|             }}
 | |
|             disabled={loadingTicketPdf}
 | |
|           >
 | |
|             <PrintIcon fontSize="small" sx={{ mr: 1 }} />
 | |
|             {loadingTicketPdf && <CircularProgress size={16} sx={{ mr: 1 }} />}
 | |
|             Reimprimir Ticket Liq.
 | |
|           </MenuItem>
 | |
|         )}
 | |
| 
 | |
|         {selectedRow && ( // Opción de Eliminar
 | |
|           ((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados))
 | |
|         ) && (
 | |
|             <MenuItem onClick={() => {
 | |
|               if (selectedRow) handleDelete(selectedRow.idParte);
 | |
|             }}>
 | |
|               <DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
 | |
|             </MenuItem>
 | |
|           )}
 | |
|       </Menu>
 | |
| 
 | |
|       <EntradaSalidaCanillaFormModal
 | |
|         open={modalOpen}
 | |
|         onClose={handleCloseModal}
 | |
|         onSubmit={handleModalEditSubmit}
 | |
|         initialData={editingMovimiento}
 | |
|         errorMessage={apiErrorMessage}
 | |
|         clearErrorMessage={() => setApiErrorMessage(null)}
 | |
|       />
 | |
| 
 | |
|       <Dialog open={openLiquidarDialog} onClose={handleCloseLiquidarDialog}>
 | |
|         <DialogTitle>Confirmar Liquidación</DialogTitle>
 | |
|         <DialogContent>
 | |
|           <DialogContentText>
 | |
|             Se marcarán como liquidados {selectedIdsParaLiquidar.size} movimiento(s).
 | |
|           </DialogContentText>
 | |
|           <TextField autoFocus margin="dense" id="fechaLiquidacion" label="Fecha de Liquidación" type="date"
 | |
|             fullWidth variant="standard" value={fechaLiquidacionDialog}
 | |
|             onChange={(e) => setFechaLiquidacionDialog(e.target.value)}
 | |
|             InputLabelProps={{ shrink: true }}
 | |
|           />
 | |
|         </DialogContent>
 | |
|         <DialogActions>
 | |
|           <Button onClick={handleCloseLiquidarDialog} color="secondary" disabled={loading}>Cancelar</Button>
 | |
|           <Button onClick={handleConfirmLiquidar} variant="contained" color="primary" disabled={loading || !fechaLiquidacionDialog}>
 | |
|             {loading ? <CircularProgress size={24} /> : "Liquidar"}
 | |
|           </Button>
 | |
|         </DialogActions>
 | |
|       </Dialog>
 | |
| 
 | |
|     </Box>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export default GestionarEntradasSalidasCanillaPage; |