import React, { useState, useEffect, useCallback, useMemo } 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, ToggleButtonGroup, ToggleButton } 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 { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto'; import type { CanillaDropdownDto } from '../../models/dtos/Distribucion/CanillaDropdownDto'; 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'; type TipoDestinatarioFiltro = 'canillitas' | 'accionistas'; const GestionarEntradasSalidasCanillaPage: React.FC = () => { const [movimientos, setMovimientos] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [apiErrorMessage, setApiErrorMessage] = useState(null); const [filtroFecha, setFiltroFecha] = useState(new Date().toISOString().split('T')[0]); const [filtroIdPublicacion, setFiltroIdPublicacion] = useState(''); const [filtroIdCanillitaSeleccionado, setFiltroIdCanillitaSeleccionado] = useState(''); const [filtroTipoDestinatario, setFiltroTipoDestinatario] = useState('canillitas'); const [loadingTicketPdf, setLoadingTicketPdf] = useState(false); const [publicaciones, setPublicaciones] = useState([]); const [destinatariosDropdown, setDestinatariosDropdown] = useState([]); const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false) const [modalOpen, setModalOpen] = useState(false); const [editingMovimiento, setEditingMovimiento] = useState(null); const [prefillModalData, setPrefillModalData] = useState<{ fecha?: string; idCanilla?: number | string; nombreCanilla?: string; idPublicacion?: number | string; } | null>(null); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(25); 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"); 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; }; useEffect(() => { const fetchDropdownData = async () => { if (!puedeVer) { setError("No tiene permiso para ver esta sección."); setLoading(false); // Detiene el spinner principal setLoadingFiltersDropdown(false); // Detiene el spinner de los filtros return; } setLoadingFiltersDropdown(true); setError(null); try { const pubsData = await publicacionService.getPublicacionesForDropdown(true); setPublicaciones(pubsData); // La carga de destinatarios se hará en el otro useEffect } catch (err) { console.error("Error cargando publicaciones para filtro:", err); setError("Error al cargar publicaciones."); } finally { // La carga finaliza cuando se cargan los destinatarios también. } }; fetchDropdownData(); }, [puedeVer]); // << CAMBIO: Añadir `puedeVer` como dependencia const fetchDestinatariosParaDropdown = useCallback(async () => { if (!puedeVer) { return; } setLoadingFiltersDropdown(true); setFiltroIdCanillitaSeleccionado(''); setDestinatariosDropdown([]); setError(null); try { const esAccionistaFilter = filtroTipoDestinatario === 'accionistas'; const data = await canillaService.getAllDropdownCanillas(true, esAccionistaFilter); setDestinatariosDropdown(data); } catch (err) { console.error("Error cargando destinatarios para filtro:", err); setError("Error al cargar canillitas/accionistas."); } finally { setLoadingFiltersDropdown(false); } }, [filtroTipoDestinatario, puedeVer]); useEffect(() => { fetchDestinatariosParaDropdown(); }, [fetchDestinatariosParaDropdown]); const cargarMovimientos = useCallback(async () => { 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 { const params = { fechaDesde: filtroFecha, fechaHasta: filtroFecha, idPublicacion: filtroIdPublicacion ? Number(filtroIdPublicacion) : null, idCanilla: Number(filtroIdCanillitaSeleccionado), liquidados: null, incluirNoLiquidados: null, }; const data = await entradaSalidaCanillaService.getAllEntradasSalidasCanilla(params); setMovimientos(data); setSelectedIdsParaLiquidar(new Set()); } catch (err) { 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); } }, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]); const handleOpenModal = (item?: EntradaSalidaCanillaDto) => { 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 { const canillitaSeleccionado = destinatariosDropdown.find( c => c.idCanilla === Number(filtroIdCanillitaSeleccionado) ); setEditingMovimiento(null); setPrefillModalData({ fecha: filtroFecha, idCanilla: filtroIdCanillitaSeleccionado, nombreCanilla: canillitaSeleccionado?.nomApe, idPublicacion: filtroIdPublicacion }); } 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) => { 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) { return; } if (!fechaLiquidacionDialog) { return; } 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) { 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()) { 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 = { idsPartesALiquidar: Array.from(selectedIdsParaLiquidar), fechaLiquidacion: fechaLiquidacionDialog }; 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 && !movimientoParaTicket.canillaEsAccionista) { console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla); await handleImprimirTicketLiquidacion( movimientoParaTicket.idCanilla, movimientoParaTicket.fecha, 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 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); } }; 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); setPrefillModalData(null); if (!apiErrorMessage) { cargarMovimientos(); } }; const handleImprimirTicketLiquidacion = useCallback(async ( idCanilla: number, fecha: string, esAccionista: boolean ) => { setLoadingTicketPdf(true); setApiErrorMessage(null); try { 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."; 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:", error); const message = axios.isAxiosError(error) && error.response?.data?.message ? error.response.data.message : 'Error al generar ticket.'; setApiErrorMessage(message); } finally { setLoadingTicketPdf(false); } }, []); 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); const totalARendirVisible = useMemo(() => displayData.filter(m => !m.liquidado).reduce((sum, item) => sum + item.montoARendir, 0) , [displayData]); if (!puedeVer) { return ( {error || "No tiene permiso para acceder a esta sección."} ); } const numSelectedToLiquidate = selectedIdsParaLiquidar.size; const numNotLiquidatedOnPage = displayData.filter(m => !m.liquidado).length; return ( Entradas/Salidas Canillitas & Accionistas Filtros setFiltroFecha(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} required error={!filtroFecha} helperText={!filtroFecha ? "Fecha es obligatoria" : ""} /> { if (newValue !== null) { setFiltroTipoDestinatario(newValue); } }} aria-label="Tipo de Destinatario" size="small" > Canillitas Accionistas {filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'} {!filtroIdCanillitaSeleccionado && Selección obligatoria} Publicación (Opcional) {puedeCrear && ( )} {puedeLiquidar && numSelectedToLiquidate > 0 && movimientos.some(m => selectedIdsParaLiquidar.has(m.idParte) && !m.liquidado) && ( )} {!filtroFecha && Por favor, seleccione una fecha.} {filtroFecha && !filtroIdCanillitaSeleccionado && Por favor, seleccione un {filtroTipoDestinatario === 'canillitas' ? 'canillita' : 'accionista'}.} {loading && } {error && !loading && !apiErrorMessage && {error}} {apiErrorMessage && {apiErrorMessage}} {loadingTicketPdf && ( Cargando ticket... )} {!loading && movimientos.length > 0 && ( Total a Liquidar: {totalARendirVisible.toLocaleString('es-AR', { style: 'currency', currency: 'ARS' })} )} {!loading && !error && puedeVer && filtroFecha && filtroIdCanillitaSeleccionado && ( {puedeLiquidar && ( 0 && numSelectedToLiquidate < numNotLiquidatedOnPage && numNotLiquidatedOnPage > 0} checked={numNotLiquidatedOnPage > 0 && numSelectedToLiquidate === numNotLiquidatedOnPage} onChange={handleSelectAllForLiquidar} disabled={numNotLiquidatedOnPage === 0} /> )} Fecha Publicación {filtroTipoDestinatario === 'canillitas' ? 'Canillita' : 'Accionista'} Salida Entrada Vendidos A Rendir Liquidado F. Liq. Obs. {(puedeModificar || puedeEliminar || puedeLiquidar) && Acciones} {displayData.length === 0 ? ( No se encontraron movimientos con los filtros aplicados. ) : ( displayData.map((m) => ( {puedeLiquidar && ( 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()} disabled={m.liquidado && !puedeEliminarLiquidados && !puedeLiquidar} > )} )))}
)} {puedeModificar && selectedRow && !selectedRow.liquidado && ( { handleOpenModal(selectedRow); handleMenuClose(); }}> Modificar)} {selectedRow && selectedRow.liquidado && puedeLiquidar && ( { if (selectedRow) { // Usar siempre selectedRow.fecha, que es la fecha original del movimiento handleImprimirTicketLiquidacion( selectedRow.idCanilla, selectedRow.fecha, // Usar siempre la fecha del movimiento selectedRow.canillaEsAccionista ); } handleMenuClose(); }} disabled={loadingTicketPdf} > {loadingTicketPdf && } Reimprimir Ticket Liq. )} {selectedRow && ( ((!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;