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([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [apiErrorMessage, setApiErrorMessage] = useState(null); const [filtroFechaDesde, setFiltroFechaDesde] = useState(new Date().toISOString().split('T')[0]); const [filtroFechaHasta, setFiltroFechaHasta] = useState(new Date().toISOString().split('T')[0]); const [filtroIdPublicacion, setFiltroIdPublicacion] = useState(''); const [filtroIdCanilla, setFiltroIdCanilla] = useState(''); const [filtroEstadoLiquidacion, setFiltroEstadoLiquidacion] = useState<'todos' | 'liquidados' | 'noLiquidados'>('noLiquidados'); const [loadingTicketPdf, setLoadingTicketPdf] = useState(false); const [publicaciones, setPublicaciones] = useState([]); const [canillitas, setCanillitas] = useState([]); const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [editingMovimiento, setEditingMovimiento] = useState(null); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); const [anchorEl, setAnchorEl] = useState(null); const [selectedRow, setSelectedRow] = useState(null); const [selectedIdsParaLiquidar, setSelectedIdsParaLiquidar] = useState>(new Set()); const [fechaLiquidacionDialog, setFechaLiquidacionDialog] = useState(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, 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) => { 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) => { setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); }; const displayData = movimientos.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); if (!loading && !puedeVer && !loadingFiltersDropdown) return {error || "Acceso denegado."}; 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 ( Entradas/Salidas Canillitas Filtros setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} /> setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} /> Publicación Canillita Estado Liquidación {puedeCrear && ()} {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && numSelectedToLiquidate > 0 && ( )} {loading && } {error && !loading && !apiErrorMessage && {error}} {apiErrorMessage && {apiErrorMessage}} {loadingTicketPdf && Cargando ticket... } {!loading && !error && puedeVer && ( {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && ( 0 && numSelectedToLiquidate < numNotLiquidatedOnPage && numNotLiquidatedOnPage > 0} checked={numNotLiquidatedOnPage > 0 && numSelectedToLiquidate === numNotLiquidatedOnPage} onChange={handleSelectAllForLiquidar} disabled={numNotLiquidatedOnPage === 0} /> )} Fecha Publicación Canillita Salida Entrada Vendidos A Rendir Liquidado F. Liq. Obs. {(puedeModificar || puedeEliminar || puedeLiquidar) && Acciones} {displayData.length === 0 ? ( No se encontraron movimientos. ) : ( displayData.map((m) => ( {puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && ( handleSelectRowForLiquidar(m.idParte)} disabled={m.liquidado} /> )} {formatDate(m.fecha)} {m.nombrePublicacion} {m.nomApeCanilla} {m.cantSalida} {m.cantEntrada} {m.vendidos} {m.montoARendir.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })} {m.liquidado ? : } {m.fechaLiquidado ? formatDate(m.fechaLiquidado) : '-'} {m.observacion || '-'} {(puedeModificar || puedeEliminar || puedeLiquidar) && ( 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 > )} )))}
)} {puedeModificar && selectedRow && !selectedRow.liquidado && ( { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} {/* Opción de Imprimir Ticket Liq. */} {selectedRow && selectedRow.liquidado && ( // Solo mostrar si ya está liquidado (para reimprimir) { 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} > {loadingTicketPdf && } Reimprimir Ticket Liq. )} {selectedRow && ( // Opción de Eliminar ((!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados)) ) && ( { if (selectedRow) handleDelete(selectedRow.idParte); }}> Eliminar )} setApiErrorMessage(null)} /> Confirmar Liquidación Se marcarán como liquidados {selectedIdsParaLiquidar.size} movimiento(s). setFechaLiquidacionDialog(e.target.value)} InputLabelProps={{ shrink: true }} />
); }; export default GestionarEntradasSalidasCanillaPage;