1. Funcionalidad Principal: Auditoría General

Se creó una nueva sección de "Auditoría" en la aplicación, diseñada para ser accedida por SuperAdmins.
Se implementó una página AuditoriaGeneralPage.tsx que actúa como un visor centralizado para el historial de cambios de múltiples entidades del sistema.
2. Backend:
Nuevo Controlador (AuditoriaController.cs): Centraliza los endpoints para obtener datos de las tablas de historial (_H).
Servicios y Repositorios Extendidos:
Se añadieron métodos GetHistorialAsync y ObtenerHistorialAsync a las capas de repositorio y servicio para cada una de las siguientes entidades, permitiendo consultar sus tablas _H con filtros:
Usuarios (gral_Usuarios_H)
Pagos de Distribuidores (cue_PagosDistribuidor_H)
Notas de Crédito/Débito (cue_CreditosDebitos_H)
Entradas/Salidas de Distribuidores (dist_EntradasSalidas_H)
Entradas/Salidas de Canillitas (dist_EntradasSalidasCanillas_H)
Novedades de Canillitas (dist_dtNovedadesCanillas_H)
Tipos de Pago (cue_dtTipopago_H)
Canillitas (Maestro) (dist_dtCanillas_H)
Distribuidores (Maestro) (dist_dtDistribuidores_H)
Empresas (Maestro) (dist_dtEmpresas_H)
Zonas (Maestro) (dist_dtZonas_H)
Otros Destinos (Maestro) (dist_dtOtrosDestinos_H)
Publicaciones (Maestro) (dist_dtPublicaciones_H)
Secciones de Publicación (dist_dtPubliSecciones_H)
Precios de Publicación (dist_Precios_H)
Recargos por Zona (dist_RecargoZona_H)
Porcentajes Pago Distribuidores (dist_PorcPago_H)
Porcentajes/Montos Canillita (dist_PorcMonPagoCanilla_H)
Control de Devoluciones (dist_dtCtrlDevoluciones_H)
Tipos de Bobina (bob_dtBobinas_H)
Estados de Bobina (bob_dtEstadosBobinas_H)
Plantas de Impresión (bob_dtPlantas_H)
Stock de Bobinas (bob_StockBobinas_H)
Tiradas (Registro Principal) (bob_RegTiradas_H)
Secciones de Tirada (bob_RegPublicaciones_H)
Cambios de Parada de Canillitas (dist_CambiosParadasCanillas_H)
Ajustes Manuales de Saldo (cue_SaldoAjustesHistorial)
DTOs de Historial: Se crearon DTOs específicos para cada tabla de historial (ej. UsuarioHistorialDto, PagoDistribuidorHistorialDto, etc.) para transferir los datos al frontend, incluyendo el nombre del usuario que realizó la modificación.
Corrección de Lógica de Saldos: Se revisó y corrigió la lógica de afectación de saldos en los servicios PagoDistribuidorService y NotaCreditoDebitoService para asegurar que los débitos y créditos se apliquen correctamente.
3. Frontend:
Nuevo Servicio (auditoriaService.ts): Contiene métodos para llamar a cada uno de los nuevos endpoints de auditoría del backend.
Nueva Página (AuditoriaGeneralPage.tsx):
Permite al SuperAdmin seleccionar el "Tipo de Entidad" a auditar desde un dropdown.
Ofrece filtros comunes (rango de fechas, usuario modificador, tipo de acción) y filtros específicos que aparecen dinámicamente según la entidad seleccionada.
Utiliza un DataGrid de Material-UI para mostrar el historial, con columnas que se adaptan dinámicamente al tipo de entidad consultada.
Nuevos DTOs en TypeScript: Se crearon las interfaces correspondientes a los DTOs de historial del backend.
Gestión de Permisos:
La sección de Auditoría en MainLayout.tsx y su ruta en AppRoutes.tsx están protegidas para ser visibles y accesibles solo por SuperAdmins.
Se añadió un permiso de ejemplo AU_GENERAL_VIEW para ser usado si se decide extender el acceso en el futuro.
Corrección de Errores Menores: Se solucionó el problema del "parpadeo" del selector de fecha en GestionarNovedadesCanillaPage al adoptar un patrón de carga de datos más controlado, similar a otras páginas funcionales.
This commit is contained in:
2025-06-12 19:36:21 -03:00
parent 437b1e8864
commit b04a3b99bf
145 changed files with 5033 additions and 1070 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -208,7 +208,7 @@ const GestionarNotasCDPage: React.FC = () => {
<TableHead><TableRow>
<TableCell>Fecha</TableCell><TableCell>Empresa</TableCell><TableCell>Destino</TableCell>
<TableCell>Destinatario</TableCell><TableCell>Tipo</TableCell>
<TableCell align="right">Monto</TableCell><TableCell>Referencia</TableCell><TableCell>Obs.</TableCell>
<TableCell align="right">Monto</TableCell><TableCell>Referencia Interna</TableCell><TableCell>Referencia</TableCell>
{(puedeModificar || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>

View File

@@ -4,7 +4,9 @@ import { useParams, useNavigate } from 'react-router-dom';
import {
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, Chip
CircularProgress, Alert, Chip, // Alert se sigue usando para el error de carga general
ListItemIcon,
ListItemText
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
@@ -30,11 +32,11 @@ const GestionarPreciosPublicacionPage: React.FC = () => {
const [publicacion, setPublicacion] = useState<PublicacionDto | null>(null);
const [precios, setPrecios] = useState<PrecioDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); // Para errores de carga de datos
const [modalOpen, setModalOpen] = useState(false);
const [editingPrecio, setEditingPrecio] = useState<PrecioDto | null>(null); // Este estado determina si el modal edita
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
const [editingPrecio, setEditingPrecio] = useState<PrecioDto | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null); // Este se pasa al modal
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedPrecioRow, setSelectedPrecioRow] = useState<PrecioDto | null>(null);
@@ -54,7 +56,9 @@ const GestionarPreciosPublicacionPage: React.FC = () => {
return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
setLoading(true);
setError(null); // Limpiar error de carga al reintentar
setApiErrorMessage(null); // Limpiar error de API de acciones previas
try {
const [pubData, preciosData] = await Promise.all([
publicacionService.getPublicacionById(idPublicacion),
@@ -79,41 +83,48 @@ const GestionarPreciosPublicacionPage: React.FC = () => {
}, [cargarDatos]);
const handleOpenModal = (precio?: PrecioDto) => {
setEditingPrecio(precio || null); // Si hay 'precio', el modal estará en modo edición
setApiErrorMessage(null);
setEditingPrecio(precio || null);
setApiErrorMessage(null); // Limpiar error de API al abrir el modal
setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false);
setEditingPrecio(null);
// No limpiar apiErrorMessage aquí, el modal lo hace o se limpia al abrir de nuevo
};
// CORREGIDO: El segundo parámetro 'idPrecio' determina si es edición
const handleSubmitModal = async (data: CreatePrecioDto | UpdatePrecioDto, idPrecio?: number) => {
setApiErrorMessage(null);
try {
// Si idPrecio tiene valor, Y editingPrecio (initialData del modal) también lo tenía, es una actualización
if (idPrecio && editingPrecio) {
await precioService.updatePrecio(idPublicacion, idPrecio, data as UpdatePrecioDto);
} else {
await precioService.createPrecio(idPublicacion, data as CreatePrecioDto);
}
cargarDatos(); // Recargar lista
cargarDatos();
// El modal se cerrará solo si el submit es exitoso (controlado en el modal)
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al guardar el período de precio.';
setApiErrorMessage(message); throw err; // Re-lanzar para que el modal maneje el estado de error
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Error al guardar el período de precio.';
setApiErrorMessage(message); // Pasar el error al estado que se pasa al modal
throw err; // Re-lanzar para que el modal NO se cierre
}
};
const handleDelete = async (idPrecio: number) => {
if (window.confirm(`¿Está seguro de eliminar este período de precio (ID: ${idPrecio})? Esta acción puede afectar la vigencia de períodos anteriores.`)) {
setApiErrorMessage(null);
setApiErrorMessage(null); // Limpiar antes de la acción
try {
await precioService.deletePrecio(idPublicacion, idPrecio);
cargarDatos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el período de precio.';
setApiErrorMessage(message);
const message = axios.isAxiosError(err) && err.response?.data?.message
? err.response.data.message
: 'Error al eliminar el período de precio.';
setApiErrorMessage(message); // Establecer error para mostrar EN EL MODAL si fuera necesario, o en un Alert genérico de la PÁGINA SI EL MODAL NO ESTÁ ABIERTO
// Si el modal de confirmación no es parte de un modal de formulario, este apiErrorMessage se mostraría en la página.
// Como es un window.confirm, el error se mostrará en el Alert de la página principal.
}
}
handleMenuClose();
@@ -128,25 +139,38 @@ const GestionarPreciosPublicacionPage: React.FC = () => {
const formatDate = (dateString?: string | null) => {
if (!dateString) return '-';
const date = new Date(dateString); // Asegurar que se parsee correctamente si viene con hora
const date = new Date(dateString);
const day = String(date.getUTCDate()).padStart(2, '0');
const month = String(date.getUTCMonth() + 1).padStart(2, '0'); // Meses son 0-indexados
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const year = date.getUTCFullYear();
return `${day}/${month}/${year}`;
};
if (loading) {
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>;
}
if (error) {
return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>;
// Mostrar error de carga de datos de la página
if (error) {
return (
<Box sx={{p:2}}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/distribucion/publicaciones')} sx={{ mb: 2 }}>
Volver a Publicaciones
</Button>
<Alert severity="error">{error}</Alert>
</Box>
);
}
if (!puedeGestionarPrecios) {
return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>;
return (
<Box sx={{p:2}}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/distribucion/publicaciones')} sx={{ mb: 2 }}>
Volver a Publicaciones
</Button>
<Alert severity="error">No tiene permiso para gestionar precios.</Alert>
</Box>
);
}
return (
<Box sx={{ p: 2 }}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/distribucion/publicaciones')} sx={{ mb: 2 }}>
@@ -167,7 +191,12 @@ const GestionarPreciosPublicacionPage: React.FC = () => {
)}
</Paper>
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{/* --- Alert para apiErrorMessage eliminado de acá --- */}
{/* {apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>} */}
{/* Mostrar error general si es de carga Y no hay error de API de acción (para no duplicar mensajes) */}
{error && !loading && !apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
<TableContainer component={Paper}>
<Table size="small">
@@ -212,25 +241,33 @@ const GestionarPreciosPublicacionPage: React.FC = () => {
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeGestionarPrecios && selectedPrecioRow && (
<MenuItem onClick={() => { handleOpenModal(selectedPrecioRow); handleMenuClose(); }}>
<EditIcon fontSize="small" sx={{mr:1}}/> Editar Precios/Cerrar Período
<ListItemIcon><EditIcon fontSize="small" /></ListItemIcon>
<ListItemText>Editar Precios/Cerrar Período</ListItemText>
</MenuItem>
)}
{puedeGestionarPrecios && selectedPrecioRow && (
<MenuItem onClick={() => handleDelete(selectedPrecioRow.idPrecio)}>
<DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar Período
<ListItemIcon><DeleteIcon fontSize="small" /></ListItemIcon>
<ListItemText>Eliminar Período</ListItemText>
</MenuItem>
)}
</Menu>
{/* El Alert para errores de API de acciones como handleDelete se mostrará aquí si ocurre. */}
{/* Se podría refinar para que apiErrorMessage solo se muestre si el modal NO está abierto,
pero por ahora, si hay un error en handleDelete (modal no abierto), se mostrará aquí. */}
{apiErrorMessage && !modalOpen && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{idPublicacion &&
<PrecioFormModal
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleSubmitModal}
idPublicacion={idPublicacion}
initialData={editingPrecio} // Esto le dice al modal si está editando
errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
initialData={editingPrecio}
errorMessage={apiErrorMessage} // El modal recibe el apiErrorMessage
clearErrorMessage={() => setApiErrorMessage(null)} // El modal puede limpiar este error
/>
}
</Box>

View File

@@ -2,9 +2,9 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, Chip
Box, Typography, Button, Paper, IconButton, Menu, MenuItem,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
CircularProgress, Alert, Chip
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
@@ -47,7 +47,7 @@ const GestionarRecargosPublicacionPage: React.FC = () => {
setError("ID de Publicación inválido."); setLoading(false); return;
}
if (!puedeGestionarRecargos) {
setError("No tiene permiso para gestionar recargos."); setLoading(false); return;
setError("No tiene permiso para gestionar recargos."); setLoading(false); return;
}
setLoading(true); setError(null); setApiErrorMessage(null);
try {
@@ -70,8 +70,11 @@ const GestionarRecargosPublicacionPage: React.FC = () => {
useEffect(() => { cargarDatos(); }, [cargarDatos]);
const handleOpenModal = (recargo?: RecargoZonaDto) => {
setEditingRecargo(recargo || null); setApiErrorMessage(null); setModalOpen(true);
setEditingRecargo(recargo || null);
setApiErrorMessage(null); // Limpiar error al abrir el modal
setModalOpen(true);
};
const handleCloseModal = () => {
setModalOpen(false); setEditingRecargo(null);
};
@@ -93,13 +96,13 @@ const GestionarRecargosPublicacionPage: React.FC = () => {
const handleDelete = async (idRecargoDelRow: number) => {
if (window.confirm(`¿Seguro de eliminar este recargo (ID: ${idRecargoDelRow})? Puede afectar vigencias.`)) {
setApiErrorMessage(null);
try {
setApiErrorMessage(null);
try {
await recargoZonaService.deleteRecargoZona(idPublicacion, idRecargoDelRow);
cargarDatos();
} catch (err: any) {
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el recargo.';
setApiErrorMessage(message);
const message = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar el recargo.';
setApiErrorMessage(message);
}
}
handleMenuClose();
@@ -118,7 +121,7 @@ const GestionarRecargosPublicacionPage: React.FC = () => {
// Si viniera como DateTime completo, necesitarías parsearlo y formatearlo.
const parts = dateString.split('-');
if (parts.length === 3) {
return `${parts[2]}/${parts[1]}/${parts[0]}`; // dd/MM/yyyy
return `${parts[2]}/${parts[1]}/${parts[0]}`; // dd/MM/yyyy
}
return dateString; // Devolver como está si no es el formato esperado
};
@@ -130,64 +133,69 @@ const GestionarRecargosPublicacionPage: React.FC = () => {
return (
<Box sx={{ p: 2 }}>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate(`/distribucion/publicaciones`)} sx={{ mb: 2 }}>
Volver a Publicaciones
</Button>
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate(`/distribucion/publicaciones`)} sx={{ mb: 2 }}>
Volver a Publicaciones
</Button>
<Typography variant="h4" gutterBottom>Recargos por Zona para: {publicacion?.nombre || 'Cargando...'}</Typography>
<Typography variant="subtitle1" color="text.secondary" gutterBottom>Empresa: {publicacion?.nombreEmpresa || '-'}</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
{puedeGestionarRecargos && (
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Recargo
</Button>
{puedeGestionarRecargos && (
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()} sx={{ mb: 2 }}>
Agregar Nuevo Recargo
</Button>
)}
</Paper>
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell sx={{fontWeight: 'bold'}}>Zona</TableCell>
<TableCell sx={{fontWeight: 'bold'}}>Vigencia Desde</TableCell>
<TableCell sx={{fontWeight: 'bold'}}>Vigencia Hasta</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold'}}>Valor</TableCell>
<TableCell align="center" sx={{fontWeight: 'bold'}}>Estado</TableCell>
<TableCell align="right" sx={{fontWeight: 'bold'}}>Acciones</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Zona</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Vigencia Desde</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Vigencia Hasta</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Valor</TableCell>
<TableCell align="center" sx={{ fontWeight: 'bold' }}>Estado</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{recargos.length === 0 ? (
<TableRow><TableCell colSpan={6} align="center">No hay recargos definidos para esta publicación.</TableCell></TableRow>
) : (
recargos.sort((a, b) => new Date(b.vigenciaD).getTime() - new Date(a.vigenciaD).getTime() || a.nombreZona.localeCompare(b.nombreZona)) // Ordenar por fecha desc, luego zona asc
.map((r) => (
.map((r) => (
<TableRow key={r.idRecargo} hover>
<TableCell>{r.nombreZona}</TableCell><TableCell>{formatDate(r.vigenciaD)}</TableCell>
<TableCell>{formatDate(r.vigenciaH)}</TableCell>
<TableCell align="right">${r.valor.toFixed(2)}</TableCell>
<TableCell align="center">{!r.vigenciaH ? <Chip label="Activo" color="success" size="small" /> : <Chip label="Cerrado" size="small" />}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, r)} disabled={!puedeGestionarRecargos}><MoreVertIcon /></IconButton>
</TableCell>
<TableCell>{r.nombreZona}</TableCell><TableCell>{formatDate(r.vigenciaD)}</TableCell>
<TableCell>{formatDate(r.vigenciaH)}</TableCell>
<TableCell align="right">${r.valor.toFixed(2)}</TableCell>
<TableCell align="center">{!r.vigenciaH ? <Chip label="Activo" color="success" size="small" /> : <Chip label="Cerrado" size="small" />}</TableCell>
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, r)} disabled={!puedeGestionarRecargos}><MoreVertIcon /></IconButton>
</TableCell>
</TableRow>
)))}
)))}
</TableBody>
</Table>
</TableContainer>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{puedeGestionarRecargos && selectedRecargoRow && (
<MenuItem onClick={() => { handleOpenModal(selectedRecargoRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{mr:1}}/> Editar/Cerrar</MenuItem>)}
<MenuItem onClick={() => { handleOpenModal(selectedRecargoRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar/Cerrar</MenuItem>)}
{puedeGestionarRecargos && selectedRecargoRow && (
<MenuItem onClick={() => handleDelete(selectedRecargoRow.idRecargo)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar</MenuItem>)}
<MenuItem onClick={() => handleDelete(selectedRecargoRow.idRecargo)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)}
</Menu>
{/* Alert para errores de acciones DELETE (cuando el modal no está abierto) */ }
{apiErrorMessage && !modalOpen && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{idPublicacion &&
<RecargoZonaFormModal
open={modalOpen} onClose={handleCloseModal} onSubmit={handleSubmitModal}
idPublicacion={idPublicacion} initialData={editingRecargo}
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
open={modalOpen}
onClose={handleCloseModal}
onSubmit={handleSubmitModal}
idPublicacion={idPublicacion}
initialData={editingRecargo}
errorMessage={apiErrorMessage} // Se pasa el error al modal
clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
</Box>

View File

@@ -1,14 +1,15 @@
// src/pages/Impresion/GestionarStockBobinasPage.tsx
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
Box, Typography, TextField, Button, Paper, IconButton, Menu, MenuItem, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
CircularProgress, Alert, FormControl, InputLabel, Select
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz'; // Para cambiar estado
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import stockBobinaService from '../../services/Impresion/stockBobinaService';
import tipoBobinaService from '../../services/Impresion/tipoBobinaService';
@@ -30,13 +31,16 @@ import StockBobinaCambioEstadoModal from '../../components/Modals/Impresion/Stoc
import { usePermissions } from '../../hooks/usePermissions';
import axios from 'axios';
const ID_ESTADO_DISPONIBLE = 1;
const ID_ESTADO_UTILIZADA = 2;
const ID_ESTADO_DANADA = 3;
const GestionarStockBobinasPage: React.FC = () => {
const [stock, setStock] = useState<StockBobinaDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [apiErrorMessage, setApiErrorMessage] = useState<string | null>(null);
// Estados para filtros
const [filtroTipoBobina, setFiltroTipoBobina] = useState<number | string>('');
const [filtroNroBobina, setFiltroNroBobina] = useState('');
const [filtroPlanta, setFiltroPlanta] = useState<number | string>('');
@@ -45,22 +49,21 @@ const GestionarStockBobinasPage: React.FC = () => {
const [filtroFechaDesde, setFiltroFechaDesde] = useState<string>(new Date().toISOString().split('T')[0]);
const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]);
// Datos para dropdowns de filtros
const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]);
const [plantas, setPlantas] = useState<PlantaDto[]>([]);
const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDto[]>([]);
const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false);
const [ingresoModalOpen, setIngresoModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [cambioEstadoModalOpen, setCambioEstadoModalOpen] = useState(false);
const [selectedBobina, setSelectedBobina] = useState<StockBobinaDto | null>(null);
const [selectedBobina, setSelectedBobina] = useState<StockBobinaDto | null>(null); // Para los modales
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(25);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedBobinaForRowMenu, setSelectedBobinaForRowMenu] = useState<StockBobinaDto | null>(null); // Para el menú contextual
const { tienePermiso, isSuperAdmin } = usePermissions();
const puedeVer = isSuperAdmin || tienePermiso("IB001");
@@ -69,23 +72,22 @@ const GestionarStockBobinasPage: React.FC = () => {
const puedeModificarDatos = isSuperAdmin || tienePermiso("IB004");
const puedeEliminar = isSuperAdmin || tienePermiso("IB005");
const fetchFiltersDropdownData = useCallback(async () => {
setLoadingFiltersDropdown(true);
try {
const [tiposData, plantasData, estadosData] = await Promise.all([
tipoBobinaService.getAllTiposBobina(),
plantaService.getAllPlantas(),
estadoBobinaService.getAllEstadosBobina()
]);
setTiposBobina(tiposData);
setPlantas(plantasData);
setEstadosBobina(estadosData);
const [tiposData, plantasData, estadosData] = await Promise.all([
tipoBobinaService.getAllTiposBobina(),
plantaService.getAllPlantas(),
estadoBobinaService.getAllEstadosBobina()
]);
setTiposBobina(tiposData);
setPlantas(plantasData);
setEstadosBobina(estadosData);
} catch (err) {
console.error("Error cargando datos para filtros:", err);
setError("Error al cargar opciones de filtro.");
console.error("Error cargando datos para filtros:", err);
setError("Error al cargar opciones de filtro.");
} finally {
setLoadingFiltersDropdown(false);
setLoadingFiltersDropdown(false);
}
}, []);
@@ -120,7 +122,6 @@ const GestionarStockBobinasPage: React.FC = () => {
cargarStock();
}, [cargarStock]);
// Handlers para modales
const handleOpenIngresoModal = () => { setApiErrorMessage(null); setIngresoModalOpen(true); };
const handleCloseIngresoModal = () => setIngresoModalOpen(false);
const handleSubmitIngresoModal = async (data: CreateStockBobinaDto) => {
@@ -129,41 +130,78 @@ const GestionarStockBobinasPage: React.FC = () => {
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al ingresar bobina.'; setApiErrorMessage(msg); throw err; }
};
const handleOpenEditModal = (bobina: StockBobinaDto) => {
setSelectedBobina(bobina); setApiErrorMessage(null); setEditModalOpen(true); handleMenuClose();
const handleOpenEditModal = (bobina: StockBobinaDto | null) => {
if (!bobina) return;
setSelectedBobina(bobina);
setApiErrorMessage(null);
setEditModalOpen(true);
};
const handleCloseEditModal = () => { setEditModalOpen(false); setSelectedBobina(null); };
const handleCloseEditModal = () => {
setEditModalOpen(false);
setSelectedBobina(null);
// Devolver el foco al botón que abrió el menú (si el modal se abrió desde el menú)
if (lastOpenedMenuButtonRef.current) {
setTimeout(() => { // setTimeout puede ayudar
lastOpenedMenuButtonRef.current?.focus();
}, 0);
}
};
const handleSubmitEditModal = async (idBobina: number, data: UpdateStockBobinaDto) => {
setApiErrorMessage(null);
try { await stockBobinaService.updateDatosBobinaDisponible(idBobina, data); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al actualizar bobina.'; setApiErrorMessage(msg); throw err; }
};
const handleOpenCambioEstadoModal = (bobina: StockBobinaDto) => {
setSelectedBobina(bobina); setApiErrorMessage(null); setCambioEstadoModalOpen(true); handleMenuClose();
const handleOpenCambioEstadoModal = (bobina: StockBobinaDto | null) => {
if (!bobina) return;
setSelectedBobina(bobina);
setApiErrorMessage(null);
setCambioEstadoModalOpen(true);
};
const handleCloseCambioEstadoModal = () => { setCambioEstadoModalOpen(false); setSelectedBobina(null); };
const handleSubmitCambioEstadoModal = async (idBobina: number, data: CambiarEstadoBobinaDto) => {
setApiErrorMessage(null);
setApiErrorMessage(null);
try { await stockBobinaService.cambiarEstadoBobina(idBobina, data); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al cambiar estado.'; setApiErrorMessage(msg); throw err; }
};
const handleDeleteBobina = async (idBobina: number) => {
if (window.confirm(`¿Seguro de eliminar este ingreso de bobina (ID: ${idBobina})? Solo se permite si está 'Disponible'.`)) {
setApiErrorMessage(null);
try { await stockBobinaService.deleteIngresoBobina(idBobina); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
const handleDeleteBobina = async (bobina: StockBobinaDto | null) => {
if (!bobina) return;
// Permitir eliminar si está Disponible (1) o Dañada (3)
if (bobina.idEstadoBobina !== ID_ESTADO_DISPONIBLE && bobina.idEstadoBobina !== ID_ESTADO_DANADA) {
alert("Solo se pueden eliminar bobinas en estado 'Disponible' o 'Dañada'.");
handleMenuClose();
return;
}
if (window.confirm(`¿Seguro de eliminar este ingreso de bobina (ID: ${bobina.idBobina})?`)) {
setApiErrorMessage(null);
try { await stockBobinaService.deleteIngresoBobina(bobina.idBobina); cargarStock(); }
catch (err: any) { const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : 'Error al eliminar.'; setApiErrorMessage(msg); }
}
handleMenuClose();
};
const lastOpenedMenuButtonRef = React.useRef<HTMLButtonElement | null>(null);
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, bobina: StockBobinaDto) => {
setAnchorEl(event.currentTarget); setSelectedBobina(bobina);
const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>, bobina: StockBobinaDto) => {
setAnchorEl(event.currentTarget);
setSelectedBobinaForRowMenu(bobina);
lastOpenedMenuButtonRef.current = event.currentTarget; // Guardar el botón que abrió el menú
};
const handleMenuClose = () => {
setAnchorEl(null); setSelectedBobina(null);
setAnchorEl(null);
// No es estrictamente necesario limpiar selectedBobinaForRowMenu aquí,
// ya que se actualiza en el siguiente handleMenuOpen.
// Pero se puede ser explícito:
setSelectedBobinaForRowMenu(null);
// Devolver el foco al botón que abrió el menú si existe
if (lastOpenedMenuButtonRef.current) {
setTimeout(() => { // Pequeño retraso para asegurar que el menú se haya cerrado visualmente
lastOpenedMenuButtonRef.current?.focus();
}, 0);
}
};
const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage);
@@ -171,128 +209,146 @@ const GestionarStockBobinasPage: React.FC = () => {
setRowsPerPage(parseInt(event.target.value, 10)); setPage(0);
};
const displayData = stock.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
const formatDate = (dateString?: string | null) => dateString ? new Date(dateString + 'T00:00:00Z').toLocaleDateString('es-AR') : '-';
if (!loading && !puedeVer) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>;
return (
<Box sx={{ p: 1 }}>
<Typography variant="h5" gutterBottom>Stock de Bobinas</Typography>
<Paper sx={{ p: 2, mb: 2 }}>
{/* ... (Filtros sin cambios) ... */}
<Typography variant="h6" gutterBottom>Filtros</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2}}>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}}>
<InputLabel>Tipo Bobina</InputLabel>
<Select value={filtroTipoBobina} label="Tipo Bobina" onChange={(e) => setFiltroTipoBobina(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todos</em></MenuItem>
{tiposBobina.map(t => <MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>)}
</Select>
</FormControl>
<TextField label="Nro. Bobina" size="small" value={filtroNroBobina} onChange={(e) => setFiltroNroBobina(e.target.value)} sx={{minWidth: 150, flexGrow: 1}}/>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}}>
<InputLabel>Planta</InputLabel>
<Select value={filtroPlanta} label="Planta" onChange={(e) => setFiltroPlanta(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todas</em></MenuItem>
{plantas.map(p => <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{minWidth: 180, flexGrow: 1}}>
<InputLabel>Estado</InputLabel>
<Select value={filtroEstadoBobina} label="Estado" onChange={(e) => setFiltroEstadoBobina(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todos</em></MenuItem>
{estadosBobina.map(e => <MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>)}
</Select>
</FormControl>
<TextField label="Remito" size="small" value={filtroRemito} onChange={(e) => setFiltroRemito(e.target.value)} sx={{minWidth: 150, flexGrow: 1}}/>
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170, flexGrow: 1}}/>
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{minWidth: 170, flexGrow: 1}}/>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, mb: 2 }}>
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
<InputLabel>Tipo Bobina</InputLabel>
<Select value={filtroTipoBobina} label="Tipo Bobina" onChange={(e) => setFiltroTipoBobina(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todos</em></MenuItem>
{tiposBobina.map(t => <MenuItem key={t.idTipoBobina} value={t.idTipoBobina}>{t.denominacion}</MenuItem>)}
</Select>
</FormControl>
<TextField label="Nro. Bobina" size="small" value={filtroNroBobina} onChange={(e) => setFiltroNroBobina(e.target.value)} sx={{ minWidth: 150, flexGrow: 1 }} />
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
<InputLabel>Planta</InputLabel>
<Select value={filtroPlanta} label="Planta" onChange={(e) => setFiltroPlanta(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todas</em></MenuItem>
{plantas.map(p => <MenuItem key={p.idPlanta} value={p.idPlanta}>{p.nombre}</MenuItem>)}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 180, flexGrow: 1 }}>
<InputLabel>Estado</InputLabel>
<Select value={filtroEstadoBobina} label="Estado" onChange={(e) => setFiltroEstadoBobina(e.target.value)} disabled={loadingFiltersDropdown}>
<MenuItem value=""><em>Todos</em></MenuItem>
{estadosBobina.map(e => <MenuItem key={e.idEstadoBobina} value={e.idEstadoBobina}>{e.denominacion}</MenuItem>)}
</Select>
</FormControl>
<TextField label="Remito" size="small" value={filtroRemito} onChange={(e) => setFiltroRemito(e.target.value)} sx={{ minWidth: 150, flexGrow: 1 }} />
<TextField label="Fecha Desde" type="date" size="small" value={filtroFechaDesde} onChange={(e) => setFiltroFechaDesde(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170, flexGrow: 1 }} />
<TextField label="Fecha Hasta" type="date" size="small" value={filtroFechaHasta} onChange={(e) => setFiltroFechaHasta(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 170, flexGrow: 1 }} />
</Box>
{/* <Button variant="outlined" onClick={cargarStock} sx={{ mr: 1 }}>Aplicar Filtros</Button>
<Button variant="outlined" color="secondary" onClick={() => { // Resetear filtros
setFiltroTipoBobina(''); setFiltroNroBobina(''); setFiltroPlanta('');
setFiltroEstadoBobina(''); setFiltroRemito(''); setFiltroFechaDesde(''); setFiltroFechaHasta('');
// cargarStock(); // Opcional: recargar inmediatamente
}}>Limpiar Filtros</Button> */}
{puedeIngresar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenIngresoModal} sx={{ mt:2 }}>Ingresar Bobina</Button>)}
{puedeIngresar && (<Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenIngresoModal} sx={{ mt: 2 }}>Ingresar Bobina</Button>)}
</Paper>
{loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>}
{error && !loading && <Alert severity="error" sx={{my: 2}}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{my: 2}}>{apiErrorMessage}</Alert>}
{error && !loading && <Alert severity="error" sx={{ my: 2 }}>{error}</Alert>}
{apiErrorMessage && <Alert severity="error" sx={{ my: 2 }}>{apiErrorMessage}</Alert>}
{!loading && !error && puedeVer && (
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Nro. Bobina</TableCell><TableCell>Tipo</TableCell><TableCell>Peso (Kg)</TableCell>
<TableCell>Planta</TableCell><TableCell>Estado</TableCell><TableCell>Remito</TableCell>
<TableCell>F. Remito</TableCell><TableCell>F. Estado</TableCell>
<TableCell>Publicación</TableCell><TableCell>Sección</TableCell>
<TableCell>Obs.</TableCell><TableCell align="right">Acciones</TableCell>
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={12} align="center">No se encontraron bobinas con los filtros aplicados.</TableCell></TableRow>
) : (
displayData.map((b) => (
<TableRow key={b.idBobina} hover>
<TableCell>{b.nroBobina}</TableCell><TableCell>{b.nombreTipoBobina}</TableCell>
<TableCell align="right">{b.peso}</TableCell><TableCell>{b.nombrePlanta}</TableCell>
<TableCell><Chip label={b.nombreEstadoBobina} size="small" color={
b.idEstadoBobina === 1 ? "success" : b.idEstadoBobina === 2 ? "primary" : b.idEstadoBobina === 3 ? "error" : "default"
}/></TableCell>
<TableCell>{b.remito}</TableCell><TableCell>{formatDate(b.fechaRemito)}</TableCell>
<TableCell>{formatDate(b.fechaEstado)}</TableCell>
<TableCell>{b.nombrePublicacion || '-'}</TableCell><TableCell>{b.nombreSeccion || '-'}</TableCell>
<TableCell>{b.obs || '-'}</TableCell>
<TableCell align="right">
<TableContainer component={Paper}>
<Table size="small">
<TableHead><TableRow>
<TableCell>Nro. Bobina</TableCell><TableCell>Tipo</TableCell><TableCell>Peso (Kg)</TableCell>
<TableCell>Planta</TableCell><TableCell>Estado</TableCell><TableCell>Remito</TableCell>
<TableCell>F. Remito</TableCell><TableCell>F. Estado</TableCell>
<TableCell>Publicación</TableCell><TableCell>Sección</TableCell>
<TableCell>Obs.</TableCell>
{/* Mostrar columna de acciones si tiene algún permiso de acción */}
{(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) && <TableCell align="right">Acciones</TableCell>}
</TableRow></TableHead>
<TableBody>
{displayData.length === 0 ? (
<TableRow><TableCell colSpan={(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) ? 12 : 11} align="center">No se encontraron bobinas con los filtros aplicados.</TableCell></TableRow>
) : (
displayData.map((b) => (
<TableRow key={b.idBobina} hover>
<TableCell>{b.nroBobina}</TableCell><TableCell>{b.nombreTipoBobina}</TableCell>
<TableCell align="right">{b.peso}</TableCell><TableCell>{b.nombrePlanta}</TableCell>
<TableCell><Chip label={b.nombreEstadoBobina} size="small" color={
b.idEstadoBobina === ID_ESTADO_DISPONIBLE ? "success" : b.idEstadoBobina === ID_ESTADO_UTILIZADA ? "primary" : b.idEstadoBobina === ID_ESTADO_DANADA ? "error" : "default"
} /></TableCell>
<TableCell>{b.remito}</TableCell><TableCell>{formatDate(b.fechaRemito)}</TableCell>
<TableCell>{formatDate(b.fechaEstado)}</TableCell>
<TableCell>{b.nombrePublicacion || '-'}</TableCell><TableCell>{b.nombreSeccion || '-'}</TableCell>
<TableCell>{b.obs || '-'}</TableCell>
{(puedeModificarDatos || puedeCambiarEstado || puedeEliminar) && (
<TableCell align="right">
<IconButton onClick={(e) => handleMenuOpen(e, b)}
disabled={!puedeModificarDatos && !puedeCambiarEstado && !puedeEliminar}
// El botón de menú se deshabilita si no hay NINGUNA acción posible para esa fila
disabled={
!(b.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos) &&
!(puedeCambiarEstado) && // Siempre se puede intentar cambiar estado (el modal lo validará)
!((b.idEstadoBobina === ID_ESTADO_DISPONIBLE || b.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar)
}
><MoreVertIcon /></IconButton>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[25, 50, 100]} component="div" count={stock.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
</TableCell>
)}
</TableRow>
)))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[25, 50, 100]} component="div" count={stock.length}
rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage} labelRowsPerPage="Filas por página:"
/>
</TableContainer>
)}
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
{selectedBobina?.idEstadoBobina === 1 && puedeModificarDatos && (
<MenuItem onClick={() => handleOpenEditModal(selectedBobina!)}><EditIcon fontSize="small" sx={{mr:1}}/> Editar Datos</MenuItem>)}
{selectedBobina?.idEstadoBobina !== 3 && puedeCambiarEstado && ( // No se puede cambiar estado si está dañada
<MenuItem onClick={() => handleOpenCambioEstadoModal(selectedBobina!)}><SwapHorizIcon fontSize="small" sx={{mr:1}}/> Cambiar Estado</MenuItem>)}
{selectedBobina?.idEstadoBobina === 1 && puedeEliminar && (
<MenuItem onClick={() => handleDeleteBobina(selectedBobina!.idBobina)}><DeleteIcon fontSize="small" sx={{mr:1}}/> Eliminar Ingreso</MenuItem>)}
{selectedBobina && selectedBobina.idEstadoBobina === 3 && (!puedeModificarDatos && !puedeCambiarEstado && !puedeEliminar) && <MenuItem disabled>Sin acciones</MenuItem>}
{selectedBobina && selectedBobina.idEstadoBobina !== 1 && selectedBobina.idEstadoBobina !== 3 && (!puedeCambiarEstado) && <MenuItem disabled>Sin acciones</MenuItem>}
{selectedBobinaForRowMenu && puedeModificarDatos && selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && (
<MenuItem onClick={() => { handleOpenEditModal(selectedBobinaForRowMenu); handleMenuClose(); }}>
<EditIcon fontSize="small" sx={{ mr: 1 }} /> Editar Datos
</MenuItem>
)}
{/* --- CAMBIO: Permitir cambiar estado incluso si está Dañada --- */}
{selectedBobinaForRowMenu && puedeCambiarEstado && (
<MenuItem onClick={() => { handleOpenCambioEstadoModal(selectedBobinaForRowMenu); handleMenuClose(); }}>
<SwapHorizIcon fontSize="small" sx={{ mr: 1 }} /> Cambiar Estado
</MenuItem>
)}
{/* --- CAMBIO: Permitir eliminar si está Disponible o Dañada --- */}
{selectedBobinaForRowMenu && puedeEliminar &&
(selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && (
<MenuItem onClick={() => handleDeleteBobina(selectedBobinaForRowMenu)}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar Ingreso
</MenuItem>
)}
{/* Lógica para el MenuItem "Sin acciones" */}
{selectedBobinaForRowMenu &&
!((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE && puedeModificarDatos)) &&
!(puedeCambiarEstado) &&
!(((selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DISPONIBLE || selectedBobinaForRowMenu.idEstadoBobina === ID_ESTADO_DANADA) && puedeEliminar)) &&
<MenuItem disabled>Sin acciones disponibles</MenuItem>
}
</Menu>
<StockBobinaIngresoFormModal
open={ingresoModalOpen} onClose={handleCloseIngresoModal} onSubmit={handleSubmitIngresoModal}
errorMessage={apiErrorMessage} clearErrorMessage={() => setApiErrorMessage(null)}
/>
{selectedBobina && editModalOpen &&
{editModalOpen && selectedBobina &&
<StockBobinaEditFormModal
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
initialData={selectedBobina} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
open={editModalOpen} onClose={handleCloseEditModal} onSubmit={handleSubmitEditModal}
initialData={selectedBobina} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
{selectedBobina && cambioEstadoModalOpen &&
{cambioEstadoModalOpen && selectedBobina &&
<StockBobinaCambioEstadoModal
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
bobinaActual={selectedBobina} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
open={cambioEstadoModalOpen} onClose={handleCloseCambioEstadoModal} onSubmit={handleSubmitCambioEstadoModal}
bobinaActual={selectedBobina} errorMessage={apiErrorMessage}
clearErrorMessage={() => setApiErrorMessage(null)}
/>
}
</Box>