Finalización de Reportes y arreglos varios de controles y comportamientos...

This commit is contained in:
2025-06-03 13:45:20 -03:00
parent 99532b03f1
commit 062cc05fd0
67 changed files with 4523 additions and 993 deletions

View File

@@ -1,4 +1,3 @@
// src/pages/Distribucion/GestionarEntradasSalidasCanillaPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import {
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
@@ -7,27 +6,27 @@ import {
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'; // Para Liquidar
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 { CreateEntradaSalidaCanillaDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaCanillaDto';
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[]>([]);
@@ -35,13 +34,12 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Filtros
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); //useState('');
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[]>([]);
@@ -58,9 +56,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const [fechaLiquidacionDialog, setFechaLiquidacionDialog] = useState<string>(new Date().toISOString().split('T')[0]);
const [openLiquidarDialog, setOpenLiquidarDialog] = useState(false);
const { tienePermiso, isSuperAdmin } = usePermissions();
// MC001 (Ver), MC002 (Crear), MC003 (Modificar), MC004 (Eliminar), MC005 (Liquidar)
const puedeVer = isSuperAdmin || tienePermiso("MC001");
const puedeCrear = isSuperAdmin || tienePermiso("MC002");
const puedeModificar = isSuperAdmin || tienePermiso("MC003");
@@ -68,6 +64,17 @@ 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]}`;
}
return datePart;
};
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
@@ -120,22 +127,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
const handleOpenModal = (item?: EntradaSalidaCanillaDto) => {
setEditingMovimiento(item || null); setApiErrorMessage(null); setModalOpen(true);
};
const handleCloseModal = () => { setModalOpen(false); setEditingMovimiento(null); };
const handleSubmitModal = async (data: CreateEntradaSalidaCanillaDto | UpdateEntradaSalidaCanillaDto, idParte?: number) => {
setApiErrorMessage(null);
try {
if (idParte && editingMovimiento) {
await entradaSalidaCanillaService.updateEntradaSalidaCanilla(idParte, data as UpdateEntradaSalidaCanillaDto);
} else {
await entradaSalidaCanillaService.createEntradaSalidaCanilla(data as CreateEntradaSalidaCanillaDto);
}
cargarMovimientos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar.';
setApiErrorMessage(message); throw err;
}
};
const handleDelete = async (idParte: number) => {
if (window.confirm(`¿Seguro de eliminar este movimiento (ID: ${idParte})?`)) {
@@ -147,7 +138,10 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
};
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => {
setAnchorEl(event.currentTarget); setSelectedRow(item);
// 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); };
@@ -177,40 +171,150 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
};
const handleCloseLiquidarDialog = () => setOpenLiquidarDialog(false);
const handleConfirmLiquidar = async () => {
setApiErrorMessage(null); setLoading(true);
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
fechaLiquidacion: fechaLiquidacionDialog // El backend espera YYYY-MM-DD
};
try {
await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto);
cargarMovimientos(); // Recargar para ver los cambios
setOpenLiquidarDialog(false);
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);
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);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
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: 2 }}>
<Typography variant="h4" gutterBottom>Entradas/Salidas Canillitas</Typography>
<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 }}>
@@ -250,36 +354,62 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{loadingTicketPdf &&
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}>
<CircularProgress size={20} sx={{ mr: 1 }} />
<Typography variant="body2">Cargando ticket...</Typography>
</Box>
}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' &&
<TableCell padding="checkbox">
<Checkbox
indeterminate={numSelectedToLiquidate > 0 && numSelectedToLiquidate < numNotLiquidatedOnPage}
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) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<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' ? 12 : 11} align="center">No se encontraron movimientos.</TableCell></TableRow>
<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' &&
{puedeLiquidar && filtroEstadoLiquidacion !== 'liquidados' && (
<TableCell padding="checkbox">
<Checkbox
checked={selectedIdsParaLiquidar.has(m.idParte)}
@@ -287,29 +417,34 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => {
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.toFixed(2)}</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) && (
<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)}
disabled={
// Deshabilitar si no tiene ningún permiso de eliminación O
// si está liquidado y no tiene permiso para eliminar liquidados
!((!m.liquidado && puedeEliminar) || (m.liquidado && puedeEliminarLiquidados))
}
>
<MoreVertIcon />
</IconButton>
<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>
@@ -327,18 +462,45 @@ 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>)}
{selectedRow && (
(!selectedRow.liquidado && puedeEliminar) || (selectedRow.liquidado && puedeEliminarLiquidados)
{/* 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={() => handleDelete(selectedRow.idParte)}>
<MenuItem onClick={() => {
if (selectedRow) handleDelete(selectedRow.idParte);
}}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar
</MenuItem>
)}
</Menu>
<EntradaSalidaCanillaFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
initialData={editingMovimiento} errorMessage={apiErrorMessage}
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleModalEditSubmit}
initialData={editingMovimiento}
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>