Implementación AnomalIA - Fix de dropdowns y permisos.
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 5m17s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 5m17s
				
			This commit is contained in:
		| @@ -44,6 +44,7 @@ const getModuloConceptualDelPermiso = (permisoModulo: string): string => { | ||||
|     if (moduloLower.includes("impresión tiradas") || | ||||
|         moduloLower.includes("impresión bobinas") || // Cubre "Impresión Bobinas" y "Tipos Bobinas" | ||||
|         moduloLower.includes("impresión plantas") || | ||||
|         moduloLower.includes("estados bobinas") || | ||||
|         moduloLower.includes("tipos bobinas")) { // Añadido explícitamente | ||||
|         return "Impresión"; | ||||
|     } | ||||
|   | ||||
| @@ -228,15 +228,12 @@ const UsuarioFormModal: React.FC<UsuarioFormModalProps> = ({ | ||||
|                      </Box> | ||||
|                 </Box> | ||||
|  | ||||
|                 <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 0.5 }}> {/* Fila 5 (Checkboxes) */} | ||||
|                      <Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}> | ||||
|                         <FormControlLabel control={<Checkbox checked={supAdmin} onChange={(e) => setSupAdmin(e.target.checked)} disabled={loading}/>} label="Super Administrador" /> | ||||
|                      </Box> | ||||
|                 <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 0.5 }}>  | ||||
|                      <Box sx={{ flex: 1, minWidth: 'calc(50% - 8px)'}}> | ||||
|                         <FormControlLabel control={<Checkbox checked={debeCambiarClave} onChange={(e) => setDebeCambiarClave(e.target.checked)} disabled={loading}/>} label="Debe Cambiar Clave" /> | ||||
|                     </Box> | ||||
|                 </Box> | ||||
|             </Box> {/* Fin contenedor principal de campos */} | ||||
|             </Box> | ||||
|  | ||||
|  | ||||
|           {errorMessage && <Alert severity="error" sx={{ mt: 2, width: '100%' }}>{errorMessage}</Alert>} | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import React, { createContext, useState, useContext, type ReactNode, useEffect } from 'react'; | ||||
| import type { LoginResponseDto } from '../models/dtos/Usuarios/LoginResponseDto'; | ||||
| import React, { createContext, useState, useContext, type ReactNode, useEffect, useCallback } from 'react'; | ||||
| import { jwtDecode } from 'jwt-decode'; | ||||
| import { getAlertas, marcarAlertaLeida, marcarGrupoComoLeido, type AlertaGenericaDto } from '../services/Anomalia/alertaService'; | ||||
|  | ||||
| // Interfaz para los datos del usuario que guardaremos en el contexto | ||||
| export interface UserContextData { | ||||
|   userId: number; | ||||
|   username: string; | ||||
| @@ -11,33 +10,37 @@ export interface UserContextData { | ||||
|   debeCambiarClave: boolean; | ||||
|   perfil: string; | ||||
|   idPerfil: number; | ||||
|   permissions: string[]; // Guardamos los codAcc | ||||
|   permissions: string[]; | ||||
| } | ||||
|  | ||||
| // Interfaz para el payload decodificado del JWT | ||||
| interface DecodedJwtPayload { | ||||
|   sub: string;          // User ID (viene como string) | ||||
|   name: string;         // Username | ||||
|   given_name?: string;  // Nombre (estándar, pero verifica tu token) | ||||
|   family_name?: string; // Apellido (estándar, pero verifica tu token) | ||||
|   role: string | string[]; // Puede ser uno o varios roles | ||||
|   sub: string; | ||||
|   name: string; | ||||
|   given_name?: string; | ||||
|   family_name?: string; | ||||
|   role: string | string[]; | ||||
|   perfil: string; | ||||
|   idPerfil: string;     // (viene como string) | ||||
|   debeCambiarClave: string; // (viene como string "True" o "False") | ||||
|   permission?: string | string[]; // Nuestros claims de permiso (codAcc) | ||||
|   [key: string]: any;   // Para otros claims | ||||
|   idPerfil: string; | ||||
|   debeCambiarClave: string; | ||||
|   permission?: string | string[]; | ||||
|   [key: string]: any; | ||||
| } | ||||
|  | ||||
| interface AuthContextType { | ||||
|   isAuthenticated: boolean; | ||||
|   user: UserContextData | null; // Usar el tipo extendido | ||||
|   user: UserContextData | null; | ||||
|   token: string | null; | ||||
|   isLoading: boolean; | ||||
|   alertas: AlertaGenericaDto[]; | ||||
|   showForcedPasswordChangeModal: boolean; | ||||
|   isPasswordChangeForced: boolean; | ||||
|  | ||||
|   marcarAlertaComoLeida: (idAlerta: number) => Promise<void>; | ||||
|   marcarGrupoDeAlertasLeido: (tipoAlerta: string, idEntidad: number) => Promise<void>; | ||||
|  | ||||
|   setShowForcedPasswordChangeModal: (show: boolean) => void; | ||||
|   passwordChangeCompleted: () => void; | ||||
|   login: (apiLoginResponse: LoginResponseDto) => void; // Recibe el DTO de la API | ||||
|   login: (apiLoginResponse: any) => void; // DTO no definido aquí, usamos any | ||||
|   logout: () => void; | ||||
| } | ||||
|  | ||||
| @@ -50,24 +53,57 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => | ||||
|   const [isLoading, setIsLoading] = useState<boolean>(true); | ||||
|   const [showForcedPasswordChangeModal, setShowForcedPasswordChangeModal] = useState<boolean>(false); | ||||
|   const [isPasswordChangeForced, setIsPasswordChangeForced] = useState<boolean>(false); | ||||
|   const [alertas, setAlertas] = useState<AlertaGenericaDto[]>([]); | ||||
|  | ||||
|   const processTokenAndSetUser = (jwtToken: string) => { | ||||
|   const fetchAlertas = useCallback(async (currentUser: UserContextData | null) => { | ||||
|     if (currentUser && (currentUser.esSuperAdmin || currentUser.permissions.includes('AL001'))) { | ||||
|       try { | ||||
|         const data = await getAlertas(); | ||||
|         setAlertas(data || []); | ||||
|       } catch (error) { | ||||
|         console.error("Error al obtener alertas en AuthContext:", error); | ||||
|         setAlertas([]); | ||||
|       } | ||||
|     } else { | ||||
|       setAlertas([]); | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   const marcarAlertaComoLeida = async (idAlerta: number) => { | ||||
|     try { | ||||
|       await marcarAlertaLeida(idAlerta); | ||||
|       await fetchAlertas(user); // Refresca el estado global | ||||
|     } catch (error) { | ||||
|       console.error("Error al marcar alerta como leída:", error); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const marcarGrupoDeAlertasLeido = async (tipoAlerta: string, idEntidad: number) => { | ||||
|     try { | ||||
|       await marcarGrupoComoLeido({ tipoAlerta, idEntidad }); | ||||
|       await fetchAlertas(user); // Refresca el estado global | ||||
|     } catch (error) { | ||||
|       console.error(`Error al marcar grupo ${tipoAlerta}/${idEntidad} como leído:`, error); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const logout = useCallback(() => { | ||||
|     localStorage.removeItem('authToken'); | ||||
|     setToken(null); | ||||
|     setUser(null); | ||||
|     setIsAuthenticated(false); | ||||
|     setShowForcedPasswordChangeModal(false); | ||||
|     setIsPasswordChangeForced(false); | ||||
|     setAlertas([]); | ||||
|   }, []); | ||||
|  | ||||
|   const processTokenAndSetUser = useCallback((jwtToken: string) => { | ||||
|     try { | ||||
|       const decodedToken = jwtDecode<DecodedJwtPayload>(jwtToken); | ||||
|  | ||||
|       // Verificar expiración (opcional, pero buena práctica aquí también) | ||||
|       const currentTime = Date.now() / 1000; | ||||
|       if (decodedToken.exp && decodedToken.exp < currentTime) { | ||||
|         console.warn("Token expirado al procesar."); | ||||
|         logout(); // Llama a logout que limpia todo | ||||
|         return; | ||||
|         logout(); return; | ||||
|       } | ||||
|  | ||||
|       let permissions: string[] = []; | ||||
|       if (decodedToken.permission) { | ||||
|         permissions = Array.isArray(decodedToken.permission) ? decodedToken.permission : [decodedToken.permission]; | ||||
|       } | ||||
|  | ||||
|       const userForContext: UserContextData = { | ||||
|         userId: parseInt(decodedToken.sub, 10), | ||||
|         username: decodedToken.name, | ||||
| @@ -75,27 +111,23 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => | ||||
|         esSuperAdmin: decodedToken.role === "SuperAdmin" || (Array.isArray(decodedToken.role) && decodedToken.role.includes("SuperAdmin")), | ||||
|         debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true', | ||||
|         idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0, | ||||
|         permissions: permissions, | ||||
|         perfil: decodedToken.perfil || 'Usuario' // Asignar un valor por defecto si no existe | ||||
|         permissions: Array.isArray(decodedToken.permission) ? decodedToken.permission : (decodedToken.permission ? [decodedToken.permission] : []), | ||||
|         perfil: decodedToken.perfil || 'Usuario' | ||||
|       }; | ||||
|  | ||||
|       setToken(jwtToken); | ||||
|       setUser(userForContext); | ||||
|       setIsAuthenticated(true); | ||||
|       localStorage.setItem('authToken', jwtToken); | ||||
|       localStorage.setItem('authUser', JSON.stringify(userForContext)); // Guardar el usuario procesado | ||||
|  | ||||
|       // Lógica para el modal de cambio de clave | ||||
|       if (userForContext.debeCambiarClave) { | ||||
|         setShowForcedPasswordChangeModal(true); | ||||
|         setIsPasswordChangeForced(true); | ||||
|       } | ||||
|  | ||||
|     } catch (error) { | ||||
|       console.error("Error al decodificar o procesar token:", error); | ||||
|       logout(); // Limpiar estado si el token es inválido | ||||
|       console.error("Error al decodificar token:", error); | ||||
|       logout(); | ||||
|     } | ||||
|   }; | ||||
|   }, [logout]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     setIsLoading(true); | ||||
| @@ -104,20 +136,18 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => | ||||
|       processTokenAndSetUser(storedToken); | ||||
|     } | ||||
|     setIsLoading(false); | ||||
|   }, []); | ||||
|   }, [processTokenAndSetUser]); | ||||
|  | ||||
|   const login = (apiLoginResponse: LoginResponseDto) => { | ||||
|     processTokenAndSetUser(apiLoginResponse.token); // Procesar el token recibido | ||||
|   }; | ||||
|   useEffect(() => { | ||||
|     if (user && isAuthenticated) { | ||||
|         fetchAlertas(user); | ||||
|         const intervalId = setInterval(() => fetchAlertas(user), 300000); // Refresca cada 5 mins | ||||
|         return () => clearInterval(intervalId); | ||||
|     } | ||||
|   }, [user, isAuthenticated, fetchAlertas]); | ||||
|  | ||||
|   const logout = () => { | ||||
|     localStorage.removeItem('authToken'); | ||||
|     localStorage.removeItem('authUser'); | ||||
|     setToken(null); | ||||
|     setUser(null); | ||||
|     setIsAuthenticated(false); | ||||
|     setShowForcedPasswordChangeModal(false); | ||||
|     setIsPasswordChangeForced(false); | ||||
|   const login = (apiLoginResponse: any) => { | ||||
|     processTokenAndSetUser(apiLoginResponse.token); | ||||
|   }; | ||||
|  | ||||
|   const passwordChangeCompleted = () => { | ||||
| @@ -138,6 +168,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => | ||||
|     <AuthContext.Provider value={{ | ||||
|       isAuthenticated, user, token, isLoading, | ||||
|       showForcedPasswordChangeModal, isPasswordChangeForced, | ||||
|       alertas, marcarAlertaComoLeida, marcarGrupoDeAlertasLeido, | ||||
|       setShowForcedPasswordChangeModal, passwordChangeCompleted, | ||||
|       login, logout | ||||
|     }}> | ||||
|   | ||||
| @@ -1,14 +1,13 @@ | ||||
| // src/layouts/MainLayout.tsx | ||||
| import React, { type ReactNode, useState, useEffect, useMemo } // << AÑADIR useMemo | ||||
|     from 'react'; | ||||
| import React, { type ReactNode, useState, useEffect, useMemo } from 'react'; | ||||
| import { | ||||
|     Box, AppBar, Toolbar, Typography, Tabs, Tab, Paper, | ||||
|     IconButton, Menu, MenuItem, ListItemIcon, ListItemText, Divider, | ||||
|     Button | ||||
|     Button, Badge | ||||
| } from '@mui/material'; | ||||
| import AccountCircle from '@mui/icons-material/AccountCircle'; | ||||
| import LockResetIcon from '@mui/icons-material/LockReset'; | ||||
| import LogoutIcon from '@mui/icons-material/Logout'; | ||||
| import NotificationsIcon from '@mui/icons-material/Notifications'; | ||||
| import { useAuth } from '../contexts/AuthContext'; | ||||
| import ChangePasswordModal from '../components/Modals/Usuarios/ChangePasswordModal'; | ||||
| import { useNavigate, useLocation } from 'react-router-dom'; | ||||
| @@ -18,6 +17,16 @@ interface MainLayoutProps { | ||||
|     children: ReactNode; | ||||
| } | ||||
|  | ||||
| // --- Helper para dar nombres legibles a los tipos de alerta --- | ||||
| const getTipoAlertaLabel = (tipoAlerta: string): string => { | ||||
|     switch (tipoAlerta) { | ||||
|         case 'DevolucionAnomala': return 'Devoluciones Anómalas'; | ||||
|         case 'ComportamientoSistema': return 'Anomalías del Sistema'; | ||||
|         case 'FaltaDeDatos': return 'Falta de Datos'; | ||||
|         default: return tipoAlerta; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // Definición original de módulos | ||||
| const allAppModules = [ | ||||
|     { label: 'Inicio', path: '/', requiredPermission: null }, // Inicio siempre visible | ||||
| @@ -31,22 +40,36 @@ const allAppModules = [ | ||||
| ]; | ||||
|  | ||||
| const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|     // Obtenemos todo lo necesario del AuthContext, INCLUYENDO LAS ALERTAS | ||||
|     const { | ||||
|         user, // user ya está disponible aquí | ||||
|         logout, | ||||
|         isAuthenticated, | ||||
|         isPasswordChangeForced, | ||||
|         showForcedPasswordChangeModal, | ||||
|         setShowForcedPasswordChangeModal, | ||||
|         passwordChangeCompleted | ||||
|         user, logout, isAuthenticated, isPasswordChangeForced, | ||||
|         showForcedPasswordChangeModal, setShowForcedPasswordChangeModal, | ||||
|         passwordChangeCompleted, | ||||
|         alertas  | ||||
|     } = useAuth(); | ||||
|  | ||||
|     const { tienePermiso, isSuperAdmin } = usePermissions(); // <<--- OBTENER HOOK DE PERMISOS | ||||
|      | ||||
|     // El resto de los hooks locales no cambian | ||||
|     const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|     const navigate = useNavigate(); | ||||
|     const location = useLocation(); | ||||
|  | ||||
|     const [selectedTab, setSelectedTab] = useState<number | false>(false); | ||||
|     const [anchorElUserMenu, setAnchorElUserMenu] = useState<null | HTMLElement>(null); | ||||
|     const [anchorElAlertasMenu, setAnchorElAlertasMenu] = useState<null | HTMLElement>(null); | ||||
|  | ||||
|     // --- Agrupación de alertas para el menú --- | ||||
|     const gruposDeAlertas = useMemo(() => { | ||||
|         if (!alertas || !Array.isArray(alertas)) return []; | ||||
|          | ||||
|         const groups = alertas.reduce((acc, alerta) => { | ||||
|             const label = getTipoAlertaLabel(alerta.tipoAlerta); | ||||
|             acc[label] = (acc[label] || 0) + 1; | ||||
|             return acc; | ||||
|         }, {} as Record<string, number>); | ||||
|  | ||||
|         return Object.entries(groups); // Devuelve [['Devoluciones Anómalas', 5], ...] | ||||
|     }, [alertas]); | ||||
|  | ||||
|     const numAlertas = alertas.length; | ||||
|  | ||||
|     const accessibleModules = useMemo(() => { | ||||
|         if (!isAuthenticated) return []; | ||||
| @@ -92,6 +115,17 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|         setAnchorElUserMenu(null); | ||||
|     }; | ||||
|  | ||||
|     // Handlers para el nuevo menú de alertas | ||||
|     const handleOpenAlertasMenu = (event: React.MouseEvent<HTMLElement>) => { | ||||
|         setAnchorElAlertasMenu(event.currentTarget); | ||||
|     }; | ||||
|  | ||||
|     const handleCloseAlertasMenu = () => { | ||||
|         setAnchorElAlertasMenu(null); | ||||
|     }; | ||||
|  | ||||
|     const handleNavigateToAlertas = () => { navigate('/anomalias/alertas'); handleCloseAlertasMenu(); }; | ||||
|  | ||||
|     const handleChangePasswordClick = () => { | ||||
|         setShowForcedPasswordChangeModal(true); | ||||
|         handleCloseUserMenu(); | ||||
| @@ -133,7 +167,6 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     // Si no hay módulos accesibles después del login (y no es el cambio de clave forzado) | ||||
|     // Esto podría pasar si un usuario no tiene permiso para NINGUNA sección, ni siquiera Inicio. | ||||
|     // Deberías redirigir a login o mostrar un mensaje de "Sin acceso". | ||||
| @@ -162,6 +195,37 @@ const MainLayout: React.FC<MainLayoutProps> = ({ children }) => { | ||||
|                         )} | ||||
|                         {isAuthenticated && ( | ||||
|                             <> | ||||
|                                 <IconButton onClick={handleOpenAlertasMenu} color="inherit"> | ||||
|                                     <Badge badgeContent={numAlertas} color="error"> | ||||
|                                         <NotificationsIcon /> | ||||
|                                     </Badge> | ||||
|                                 </IconButton> | ||||
|  | ||||
|                                 <Menu | ||||
|                                     id="alertas-menu" | ||||
|                                     anchorEl={anchorElAlertasMenu} | ||||
|                                     open={Boolean(anchorElAlertasMenu)} | ||||
|                                     onClose={() => setAnchorElAlertasMenu(null)} | ||||
|                                 > | ||||
|                                     <MenuItem disabled> | ||||
|                                         <ListItemText primary={`Tienes ${numAlertas} alertas pendientes.`} /> | ||||
|                                     </MenuItem> | ||||
|                                     <Divider /> | ||||
|                                      | ||||
|                                     {gruposDeAlertas.map(([label, count]) => ( | ||||
|                                         <MenuItem key={label} onClick={handleNavigateToAlertas}> | ||||
|                                             <ListItemIcon><Badge badgeContent={count} color="error" sx={{mr: 2}} /></ListItemIcon> | ||||
|                                             <ListItemText>{label}</ListItemText> | ||||
|                                         </MenuItem> | ||||
|                                     ))} | ||||
|                                      | ||||
|                                     {numAlertas > 0 && <Divider />} | ||||
|                                      | ||||
|                                     <MenuItem onClick={handleNavigateToAlertas}> | ||||
|                                         <ListItemText sx={{textAlign: 'center'}}>Ver Todas las Alertas</ListItemText> | ||||
|                                     </MenuItem> | ||||
|                                 </Menu> | ||||
|  | ||||
|                                 <IconButton | ||||
|                                     size="large" | ||||
|                                     aria-label="Cuenta del usuario" | ||||
|   | ||||
| @@ -0,0 +1,5 @@ | ||||
| export interface CanillaDropdownDto { | ||||
|   idCanilla: number; | ||||
|   legajo?: number | null; | ||||
|   nomApe: string; | ||||
| } | ||||
| @@ -0,0 +1,4 @@ | ||||
| export interface OtroDestinoDropdownDto { | ||||
|   idDestino: number; | ||||
|   nombre: string; | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| export interface PublicacionDropdownDto { | ||||
|   idPublicacion: number; | ||||
|   nombre: string; | ||||
|   nombreEmpresa: string; | ||||
| } | ||||
| @@ -0,0 +1,4 @@ | ||||
| export interface EstadoBobinaDropdownDto { | ||||
|   idEstadoBobina: number; | ||||
|   denominacion: string; | ||||
| } | ||||
							
								
								
									
										129
									
								
								Frontend/src/pages/Anomalia/AlertasPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								Frontend/src/pages/Anomalia/AlertasPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| import React, { useMemo } from 'react'; | ||||
| import { DataGrid, type GridColDef } from '@mui/x-data-grid'; | ||||
| import { Button, Box, Typography, Paper, Accordion, AccordionSummary, AccordionDetails } from '@mui/material'; | ||||
| import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | ||||
| import { useAuth } from '../../contexts/AuthContext'; | ||||
| import { esES } from '@mui/x-data-grid/locales'; | ||||
| import type { AlertaGenericaDto } from '../../services/Anomalia/alertaService'; | ||||
|  | ||||
| const getTipoAlertaLabel = (tipoAlerta: string): string => { | ||||
|     switch (tipoAlerta) { | ||||
|         case 'DevolucionAnomala': return 'Devoluciones Anómalas'; | ||||
|         case 'ConsumoBobinaExcesivo': return 'Consumo de Bobinas Anómalo'; | ||||
|         case 'ComportamientoSistema': return 'Anomalías Generales del Sistema'; | ||||
|         case 'FaltaDeDatos': return 'Falta de Registros Críticos'; | ||||
|         default: return tipoAlerta; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| const AlertasPage: React.FC = () => { | ||||
|   const { alertas, marcarAlertaComoLeida, marcarGrupoDeAlertasLeido, isLoading } = useAuth(); | ||||
|  | ||||
|   const gruposPorTipo = useMemo(() => { | ||||
|     if (!Array.isArray(alertas)) return []; | ||||
|     return alertas.reduce((acc, alerta) => { | ||||
|         (acc[alerta.tipoAlerta] = acc[alerta.tipoAlerta] || []).push(alerta); | ||||
|         return acc; | ||||
|     }, {} as Record<string, AlertaGenericaDto[]>); | ||||
|   }, [alertas]); | ||||
|    | ||||
|   const getColumnsForType = (tipoAlerta: string): GridColDef[] => { | ||||
|     const baseColumns: GridColDef[] = [ | ||||
|         { field: 'fechaAnomalia', headerName: 'Fecha Evento', width: 150, valueFormatter: (value) => new Date(value as string).toLocaleDateString('es-AR') }, | ||||
|         { field: 'mensaje', headerName: 'Descripción', flex: 1 } | ||||
|     ]; | ||||
|      | ||||
|     // Columnas específicas para 'DevolucionAnomala' | ||||
|     if (tipoAlerta === 'DevolucionAnomala') { | ||||
|         baseColumns.push( | ||||
|             { field: 'cantidadEnviada', headerName: 'Llevados', width: 120 }, | ||||
|             { field: 'cantidadDevuelta', headerName: 'Devueltos', width: 120 }, | ||||
|             { field: 'porcentajeDevolucion', headerName: '% Dev.', width: 120, valueFormatter: (value) => `${Number(value).toFixed(2)}%` } | ||||
|         ); | ||||
|     } | ||||
|      | ||||
|     baseColumns.push({ | ||||
|       field: 'actions', | ||||
|       headerName: 'Acciones', | ||||
|       width: 150, | ||||
|       sortable: false, | ||||
|       renderCell: (params) => ( | ||||
|         <Button | ||||
|           variant="contained" | ||||
|           size="small" | ||||
|           // Llamamos a la función del contexto para marcar una SOLA alerta | ||||
|           onClick={() => marcarAlertaComoLeida(params.row.idAlerta)}  | ||||
|         > | ||||
|           Marcar Leída | ||||
|         </Button> | ||||
|       ), | ||||
|     }); | ||||
|  | ||||
|     return baseColumns; | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 2 }}> | ||||
|       <Typography variant="h5" gutterBottom>Centro de Alertas del Sistema</Typography> | ||||
|        | ||||
|       {Object.entries(gruposPorTipo).map(([tipoAlerta, alertasDelGrupo]) => { | ||||
|           const gruposPorEntidad = alertasDelGrupo.reduce((acc, alerta) => { | ||||
|               (acc[alerta.idEntidad] = acc[alerta.idEntidad] || []).push(alerta); | ||||
|               return acc; | ||||
|           }, {} as Record<number, AlertaGenericaDto[]>); | ||||
|  | ||||
|           return ( | ||||
|             <Accordion key={tipoAlerta} defaultExpanded> | ||||
|               <AccordionSummary expandIcon={<ExpandMoreIcon />}> | ||||
|                 <Typography variant="h6">{getTipoAlertaLabel(tipoAlerta)} ({alertasDelGrupo.length})</Typography> | ||||
|               </AccordionSummary> | ||||
|               <AccordionDetails sx={{display: 'flex', flexDirection: 'column', gap: 2}}> | ||||
|                 {Object.entries(gruposPorEntidad).map(([idEntidad, alertasDeEntidad]) => { | ||||
|                   const primeraAlerta = alertasDeEntidad[0]; | ||||
|                   // Para obtener un nombre de canillita legible en el título del grupo | ||||
|                   const nombreEntidad = primeraAlerta.entidad === 'Canillita'  | ||||
|                     ? primeraAlerta.mensaje.match(/'([^']+)'/)?.[1] || `ID ${idEntidad}` | ||||
|                     : `ID ${idEntidad}`; | ||||
|                    | ||||
|                   const tituloGrupo = primeraAlerta.entidad === 'Sistema'  | ||||
|                     ? 'Alertas Generales del Sistema'  | ||||
|                     : `${primeraAlerta.entidad}: ${nombreEntidad}`; | ||||
|  | ||||
|                   const rows = alertasDeEntidad.map(a => ({ ...a, id: a.idAlerta })); | ||||
|                    | ||||
|                   return ( | ||||
|                     <Paper key={idEntidad} variant="outlined" sx={{ p: 2 }}> | ||||
|                        <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1, flexWrap: 'wrap', gap: 1 }}> | ||||
|                            <Typography variant="subtitle1" sx={{fontWeight: 'bold'}}>{tituloGrupo} ({alertasDeEntidad.length} alertas)</Typography> | ||||
|                            <Button  | ||||
|                                 size="small"  | ||||
|                                 variant="outlined"  | ||||
|                                 onClick={() => marcarGrupoDeAlertasLeido(tipoAlerta, Number(idEntidad))}> | ||||
|                                 Marcar todas como leídas | ||||
|                            </Button> | ||||
|                        </Box> | ||||
|                        <Box sx={{ height: 300, width: '100%' }}> | ||||
|                             <DataGrid | ||||
|                                 rows={rows} | ||||
|                                 columns={getColumnsForType(tipoAlerta)} | ||||
|                                 loading={isLoading} | ||||
|                                 localeText={esES.components.MuiDataGrid.defaultProps.localeText} | ||||
|                                 density="compact" | ||||
|                             /> | ||||
|                        </Box> | ||||
|                     </Paper> | ||||
|                   ); | ||||
|                 })} | ||||
|               </AccordionDetails> | ||||
|             </Accordion> | ||||
|           ); | ||||
|       })} | ||||
|  | ||||
|       {alertas.length === 0 && !isLoading && ( | ||||
|         <Typography sx={{mt: 3, textAlign: 'center', fontStyle: 'italic'}}>No hay alertas pendientes.</Typography> | ||||
|       )} | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default AlertasPage; | ||||
| @@ -16,13 +16,14 @@ import empresaService from '../../services/Distribucion/empresaService'; | ||||
| import type { ControlDevolucionesDto } from '../../models/dtos/Distribucion/ControlDevolucionesDto'; | ||||
| import type { CreateControlDevolucionesDto } from '../../models/dtos/Distribucion/CreateControlDevolucionesDto'; | ||||
| import type { UpdateControlDevolucionesDto } from '../../models/dtos/Distribucion/UpdateControlDevolucionesDto'; | ||||
| import type { EmpresaDto } from '../../models/dtos/Distribucion/EmpresaDto'; | ||||
| import type { EmpresaDropdownDto } from '../../models/dtos/Distribucion/EmpresaDropdownDto'; | ||||
|  | ||||
| import ControlDevolucionesFormModal from '../../components/Modals/Distribucion/ControlDevolucionesFormModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
|  | ||||
| const GestionarControlDevolucionesPage: React.FC = () => { | ||||
|   // ... (estados sin cambios) ... | ||||
|   const [controles, setControles] = useState<ControlDevolucionesDto[]>([]); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
| @@ -32,8 +33,8 @@ const GestionarControlDevolucionesPage: React.FC = () => { | ||||
|   const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); | ||||
|   const [filtroIdEmpresa, setFiltroIdEmpresa] = useState<number | string>(''); | ||||
|  | ||||
|   const [empresas, setEmpresas] = useState<EmpresaDto[]>([]); | ||||
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); | ||||
|   const [empresas, setEmpresas] = useState<EmpresaDropdownDto[]>([]); | ||||
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(true); // << CAMBIO: Iniciar en true | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingControl, setEditingControl] = useState<ControlDevolucionesDto | null>(null); | ||||
| @@ -47,42 +48,58 @@ const GestionarControlDevolucionesPage: React.FC = () => { | ||||
|   const puedeVer = isSuperAdmin || tienePermiso("CD001"); | ||||
|   const puedeCrear = isSuperAdmin || tienePermiso("CD002"); | ||||
|   const puedeModificar = isSuperAdmin || tienePermiso("CD003"); | ||||
|   const puedeEliminar = isSuperAdmin || tienePermiso("CD003"); | ||||
|   // << CAMBIO: Permiso de eliminar debe ser diferente | ||||
|   const puedeEliminar = isSuperAdmin || tienePermiso("CD004"); // Asumiendo que CD004 es para eliminar | ||||
|  | ||||
|   // CORREGIDO: Función para formatear la fecha | ||||
|   // ... (formatDate sin cambios) ... | ||||
|   const formatDate = (dateString?: string | null): string => { | ||||
|     if (!dateString) return '-'; | ||||
|     // Asumimos que dateString viene del backend como "YYYY-MM-DD" o "YYYY-MM-DDTHH:mm:ss..." | ||||
|     const datePart = dateString.split('T')[0]; // Tomar solo la parte YYYY-MM-DD | ||||
|     const datePart = dateString.split('T')[0];  | ||||
|     const parts = datePart.split('-'); | ||||
|     if (parts.length === 3) { | ||||
|       // parts[0] = YYYY, parts[1] = MM, parts[2] = DD | ||||
|       return `${parts[2]}/${parts[1]}/${parts[0]}`; // Formato DD/MM/YYYY | ||||
|       return `${parts[2]}/${parts[1]}/${parts[0]}`; | ||||
|     } | ||||
|     return datePart; // Fallback si el formato no es el esperado | ||||
|     return datePart; | ||||
|   }; | ||||
|  | ||||
|  | ||||
|   const fetchFiltersDropdownData = useCallback(async () => { | ||||
|     // << CAMBIO: Guardián de permisos para la carga de filtros | ||||
|     if (!puedeVer) { | ||||
|       setError("No tiene permiso para ver esta sección."); | ||||
|       setLoading(false); // Detiene el spinner principal | ||||
|       setLoadingFiltersDropdown(false); // Detiene el spinner de filtros | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     setLoadingFiltersDropdown(true); | ||||
|     try { | ||||
|       const empresasData = await empresaService.getAllEmpresas(); | ||||
|       const empresasData = await empresaService.getEmpresasDropdown(); | ||||
|       setEmpresas(empresasData); | ||||
|     } catch (err) { | ||||
|       console.error("Error cargando empresas para filtro:", err); | ||||
|       // El error principal se manejará en cargarControles si también falla | ||||
|       setError("Error al cargar opciones de filtro."); | ||||
|     } finally { | ||||
|       setLoadingFiltersDropdown(false); | ||||
|     } | ||||
|   }, []); | ||||
|   }, [puedeVer]); // << CAMBIO: Añadir `puedeVer` como dependencia | ||||
|  | ||||
|   useEffect(() => { fetchFiltersDropdownData(); }, [fetchFiltersDropdownData]); | ||||
|   useEffect(() => { | ||||
|     fetchFiltersDropdownData(); | ||||
|   }, [fetchFiltersDropdownData]); | ||||
|  | ||||
|   const cargarControles = useCallback(async () => { | ||||
|     // El guardián aquí ya estaba y es correcto. | ||||
|     if (!puedeVer) { | ||||
|       setError("No tiene permiso para ver esta sección."); setLoading(false); return; | ||||
|       // Si ya se estableció el error en el fetch de filtros, no lo sobrescribimos. | ||||
|       if (!error) setError("No tiene permiso para ver esta sección."); | ||||
|       setLoading(false); | ||||
|       return; | ||||
|     } | ||||
|     setLoading(true); setError(null); setApiErrorMessage(null); | ||||
|  | ||||
|     setLoading(true); | ||||
|     setError(null); | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
|       const params = { | ||||
|         fechaDesde: filtroFechaDesde || null, | ||||
| @@ -92,19 +109,27 @@ const GestionarControlDevolucionesPage: React.FC = () => { | ||||
|       const data = await controlDevolucionesService.getAllControlesDevoluciones(params); | ||||
|       setControles(data); | ||||
|     } catch (err) { | ||||
|       console.error(err); setError('Error al cargar los controles de devoluciones.'); | ||||
|     } finally { setLoading(false); } | ||||
|   }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdEmpresa]); | ||||
|       console.error(err); | ||||
|       setError('Error al cargar los controles de devoluciones.'); | ||||
|     } finally { | ||||
|       setLoading(false); | ||||
|     } | ||||
|   }, [puedeVer, filtroFechaDesde, filtroFechaHasta, filtroIdEmpresa, error]); // << CAMBIO: Añadido `error` a dependencias | ||||
|  | ||||
|   useEffect(() => { cargarControles(); }, [cargarControles]); | ||||
|   useEffect(() => { | ||||
|     // Solo cargar controles si los filtros se han cargado (o intentado cargar) | ||||
|     if (!loadingFiltersDropdown) { | ||||
|         cargarControles(); | ||||
|     } | ||||
|   }, [cargarControles, loadingFiltersDropdown]); // << CAMBIO: Depende de la carga de filtros | ||||
|  | ||||
|   // ... (resto de los handlers sin cambios) ... | ||||
|   const handleOpenModal = (item?: ControlDevolucionesDto) => { | ||||
|     setEditingControl(item || null); setApiErrorMessage(null); setModalOpen(true); | ||||
|   }; | ||||
|   const handleCloseModal = () => { | ||||
|     setModalOpen(false); setEditingControl(null); | ||||
|   }; | ||||
|  | ||||
|   const handleSubmitModal = async (data: CreateControlDevolucionesDto | UpdateControlDevolucionesDto, idControl?: number) => { | ||||
|     setApiErrorMessage(null); | ||||
|     try { | ||||
| @@ -119,7 +144,6 @@ const GestionarControlDevolucionesPage: React.FC = () => { | ||||
|       setApiErrorMessage(message); throw err; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (idControl: number) => { | ||||
|     if (window.confirm(`¿Seguro de eliminar este control de devoluciones (ID: ${idControl})?`)) { | ||||
|       setApiErrorMessage(null); | ||||
| @@ -133,26 +157,35 @@ const GestionarControlDevolucionesPage: React.FC = () => { | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: ControlDevolucionesDto) => { | ||||
|     setAnchorEl(event.currentTarget); setSelectedRow(item); | ||||
|   }; | ||||
|   const handleMenuClose = () => { | ||||
|     setAnchorEl(null); setSelectedRow(null); | ||||
|   }; | ||||
|  | ||||
|   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||
|   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     setRowsPerPage(parseInt(event.target.value, 25)); setPage(0); | ||||
|     setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); | ||||
|   }; | ||||
|   // displayData ahora usará la 'controles' directamente, el formato se aplica en el renderizado | ||||
|   const displayData = controles.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); | ||||
|  | ||||
|   if (!loading && !puedeVer && !loadingFiltersDropdown) return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||
|    | ||||
|   // Si no tiene permiso, muestra solo la alerta y nada más. | ||||
|   if (!puedeVer) { | ||||
|     return ( | ||||
|         <Box sx={{ p: 2 }}> | ||||
|             <Alert severity="error"> | ||||
|                 {error || "No tiene permiso para acceder a esta sección."} | ||||
|             </Alert> | ||||
|         </Box> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ p: 1 }}> | ||||
|       <Typography variant="h5" gutterBottom>Control de Devoluciones a Empresa</Typography> | ||||
|        | ||||
|       {/* El resto del JSX se renderizará solo si 'puedeVer' es true */} | ||||
|        | ||||
|       <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 }}> | ||||
| @@ -169,12 +202,12 @@ const GestionarControlDevolucionesPage: React.FC = () => { | ||||
|         {puedeCrear && (<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenModal()}>Registrar Control</Button>)} | ||||
|       </Paper> | ||||
|  | ||||
|       {loading && <Box sx={{ display: 'flex', justifyContent: 'center', my: 2 }}><CircularProgress /></Box>} | ||||
|       {(loading || loadingFiltersDropdown) && <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>} | ||||
|  | ||||
|       {!loading && !error && puedeVer && ( | ||||
|         <TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 240px)' }}> {/* Ajusta maxHeight según sea necesario */} | ||||
|       {!loading && !loadingFiltersDropdown && !error && ( | ||||
|         <TableContainer component={Paper} sx={{ maxHeight: 'calc(100vh - 300px)' }}> {/* Ajusta maxHeight */} | ||||
|           <Table stickyHeader size="small"> | ||||
|             <TableHead><TableRow> | ||||
|               <TableCell>Fecha</TableCell><TableCell>Empresa</TableCell> | ||||
| @@ -186,7 +219,7 @@ const GestionarControlDevolucionesPage: React.FC = () => { | ||||
|             </TableRow></TableHead> | ||||
|             <TableBody> | ||||
|               {displayData.length === 0 ? ( | ||||
|                 <TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles.</TableCell></TableRow> | ||||
|                 <TableRow><TableCell colSpan={puedeModificar || puedeEliminar ? 7 : 6} align="center">No se encontraron controles con los filtros aplicados.</TableCell></TableRow> | ||||
|               ) : ( | ||||
|                 displayData.map((c) => ( | ||||
|                   <TableRow key={c.idControl} hover> | ||||
| @@ -217,7 +250,10 @@ const GestionarControlDevolucionesPage: React.FC = () => { | ||||
|         {puedeModificar && selectedRow && ( | ||||
|           <MenuItem onClick={() => { handleOpenModal(selectedRow); handleMenuClose(); }}><EditIcon fontSize="small" sx={{ mr: 1 }} /> Modificar</MenuItem>)} | ||||
|         {puedeEliminar && selectedRow && ( | ||||
|           <MenuItem onClick={() => handleDelete(selectedRow.idControl)}><DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar</MenuItem>)} | ||||
|           <MenuItem onClick={() => { if (selectedRow) handleDelete(selectedRow.idControl) }}> | ||||
|             <DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Eliminar | ||||
|           </MenuItem> | ||||
|         )} | ||||
|       </Menu> | ||||
|  | ||||
|       <ControlDevolucionesFormModal | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import React, { useState, useEffect, useCallback, useMemo } from 'react'; // << Añadido useMemo | ||||
| 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, | ||||
| @@ -21,7 +21,7 @@ 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 { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; | ||||
| import type { CanillaDropdownDto } from '../../models/dtos/Distribucion/CanillaDropdownDto'; | ||||
| import type { LiquidarMovimientosCanillaRequestDto } from '../../models/dtos/Distribucion/LiquidarMovimientosCanillaDto'; | ||||
|  | ||||
| import EntradaSalidaCanillaFormModal from '../../components/Modals/Distribucion/EntradaSalidaCanillaFormModal'; | ||||
| @@ -44,8 +44,8 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | ||||
|  | ||||
|   const [loadingTicketPdf, setLoadingTicketPdf] = useState(false); | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]); | ||||
|   const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDto[]>([]); | ||||
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); | ||||
|   const [destinatariosDropdown, setDestinatariosDropdown] = useState<CanillaDropdownDto[]>([]); | ||||
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false) | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [editingMovimiento, setEditingMovimiento] = useState<EntradaSalidaCanillaDto | null>(null); | ||||
| @@ -81,29 +81,40 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const fetchPublicaciones = async () => { | ||||
|     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); | ||||
|         console.error("Error cargando publicaciones para filtro:", err); | ||||
|         setError("Error al cargar publicaciones."); | ||||
|       } finally { | ||||
|         // No setLoadingFiltersDropdown(false) acá, esperar a la otra carga | ||||
|         // La carga finaliza cuando se cargan los destinatarios también. | ||||
|       } | ||||
|     }; | ||||
|     fetchPublicaciones(); | ||||
|   }, []); | ||||
|     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.getAllCanillas(undefined, undefined, true, esAccionistaFilter); | ||||
|       const data = await canillaService.getAllDropdownCanillas(true, esAccionistaFilter); | ||||
|       setDestinatariosDropdown(data); | ||||
|     } catch (err) { | ||||
|       console.error("Error cargando destinatarios para filtro:", err); | ||||
| @@ -111,21 +122,23 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | ||||
|     } finally { | ||||
|       setLoadingFiltersDropdown(false); | ||||
|     } | ||||
|   }, [filtroTipoDestinatario]); | ||||
|   }, [filtroTipoDestinatario, puedeVer]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     fetchDestinatariosParaDropdown(); | ||||
|   }, [fetchDestinatariosParaDropdown]); | ||||
|  | ||||
|  | ||||
|   const cargarMovimientos = useCallback(async () => { | ||||
|     if (!puedeVer) { setError("No tiene permiso para ver esta sección."); setLoading(false); return; } | ||||
|     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 = { | ||||
| @@ -148,6 +161,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | ||||
|     } | ||||
|   }, [puedeVer, filtroFecha, filtroIdPublicacion, filtroIdCanillitaSeleccionado]); | ||||
|  | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (filtroFecha && filtroIdCanillitaSeleccionado) { | ||||
|         cargarMovimientos(); | ||||
| @@ -156,8 +170,7 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | ||||
|         if (loading) setLoading(false); | ||||
|     } | ||||
|   }, [cargarMovimientos, filtroFecha, filtroIdCanillitaSeleccionado]); | ||||
|  | ||||
|  | ||||
|    | ||||
|   const handleOpenModal = (item?: EntradaSalidaCanillaDto) => { | ||||
|     if (!puedeCrear && !item) { | ||||
|         setApiErrorMessage("No tiene permiso para registrar nuevos movimientos."); | ||||
| @@ -195,7 +208,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | ||||
|     } | ||||
|     handleMenuClose(); | ||||
|   }; | ||||
|  | ||||
|   const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, item: EntradaSalidaCanillaDto) => { | ||||
|     event.currentTarget.setAttribute('data-rowid', item.idParte.toString()); | ||||
|     setAnchorEl(event.currentTarget); | ||||
| @@ -258,17 +270,15 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | ||||
|       await entradaSalidaCanillaService.liquidarMovimientos(liquidarDto); | ||||
|       setOpenLiquidarDialog(false); | ||||
|       const primerIdParteLiquidado = Array.from(selectedIdsParaLiquidar)[0]; | ||||
|       // Necesitamos encontrar el movimiento en la lista ANTES de recargar | ||||
|       const movimientoParaTicket = movimientos.find(m => m.idParte === primerIdParteLiquidado); | ||||
|  | ||||
|       await cargarMovimientos(); // Recargar la lista para reflejar el estado liquidado | ||||
|       await cargarMovimientos(); | ||||
|  | ||||
|       // Usar la fecha del movimiento original para el ticket | ||||
|       if (movimientoParaTicket && !movimientoParaTicket.canillaEsAccionista) { | ||||
|         console.log("Liquidación exitosa, generando ticket para canillita NO accionista:", movimientoParaTicket.idCanilla); | ||||
|         await handleImprimirTicketLiquidacion( | ||||
|             movimientoParaTicket.idCanilla, | ||||
|             movimientoParaTicket.fecha, // Usar la fecha del movimiento | ||||
|             movimientoParaTicket.fecha,  | ||||
|             false | ||||
|         ); | ||||
|       } else if (movimientoParaTicket && movimientoParaTicket.canillaEsAccionista) { | ||||
| @@ -328,7 +338,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | ||||
|     } finally { setLoadingTicketPdf(false); } | ||||
|   }, []); | ||||
|  | ||||
|  | ||||
|   const handleChangePage = (_event: unknown, newPage: number) => setPage(newPage); | ||||
|   const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     setRowsPerPage(parseInt(event.target.value, 10)); setPage(0); | ||||
| @@ -339,8 +348,14 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | ||||
|     displayData.filter(m => !m.liquidado).reduce((sum, item) => sum + item.montoARendir, 0) | ||||
|   , [displayData]); | ||||
|  | ||||
|   if (!loading && !puedeVer && !loadingFiltersDropdown && movimientos.length === 0 && !filtroFecha && !filtroIdCanillitaSeleccionado ) { | ||||
|      return <Box sx={{ p: 2 }}><Alert severity="error">{error || "Acceso denegado."}</Alert></Box>; | ||||
|   if (!puedeVer) { | ||||
|     return ( | ||||
|       <Box sx={{ p: 2 }}> | ||||
|         <Alert severity="error"> | ||||
|           {error || "No tiene permiso para acceder a esta sección."} | ||||
|         </Alert> | ||||
|       </Box> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   const numSelectedToLiquidate = selectedIdsParaLiquidar.size; | ||||
| @@ -352,7 +367,6 @@ const GestionarEntradasSalidasCanillaPage: React.FC = () => { | ||||
|       <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 }}> | ||||
|           {/* ... (Filtros sin cambios) ... */} | ||||
|            <TextField label="Fecha" type="date" size="small" value={filtroFecha} | ||||
|             onChange={(e) => setFiltroFecha(e.target.value)} | ||||
|             InputLabelProps={{ shrink: true }} sx={{ minWidth: 170 }} | ||||
|   | ||||
| @@ -17,8 +17,8 @@ import distribuidorService from '../../services/Distribucion/distribuidorService | ||||
| import type { EntradaSalidaDistDto } from '../../models/dtos/Distribucion/EntradaSalidaDistDto'; | ||||
| import type { CreateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/CreateEntradaSalidaDistDto'; | ||||
| import type { UpdateEntradaSalidaDistDto } from '../../models/dtos/Distribucion/UpdateEntradaSalidaDistDto'; | ||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import type { DistribuidorDto } from '../../models/dtos/Distribucion/DistribuidorDto'; | ||||
| import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto'; | ||||
| import type { DistribuidorDropdownDto } from '../../models/dtos/Distribucion/DistribuidorDropdownDto'; | ||||
|  | ||||
| import EntradaSalidaDistFormModal from '../../components/Modals/Distribucion/EntradaSalidaDistFormModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| @@ -36,8 +36,8 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { | ||||
|   const [filtroIdDistribuidor, setFiltroIdDistribuidor] = useState<number | string>(''); | ||||
|   const [filtroTipoMov, setFiltroTipoMov] = useState<'Salida' | 'Entrada' | ''>(''); | ||||
|  | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); | ||||
|   const [distribuidores, setDistribuidores] = useState<DistribuidorDto[]>([]); | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]); | ||||
|   const [distribuidores, setDistribuidores] = useState<DistribuidorDropdownDto[]>([]); | ||||
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
| @@ -69,8 +69,8 @@ const GestionarEntradasSalidasDistPage: React.FC = () => { | ||||
|     setLoadingFiltersDropdown(true); | ||||
|     try { | ||||
|       const [pubsData, distData] = await Promise.all([ | ||||
|         publicacionService.getAllPublicaciones(undefined, undefined, true), | ||||
|         distribuidorService.getAllDistribuidores() | ||||
|         publicacionService.getPublicacionesForDropdown(true), | ||||
|         distribuidorService.getAllDistribuidoresDropdown() | ||||
|       ]); | ||||
|       setPublicaciones(pubsData); | ||||
|       setDistribuidores(distData); | ||||
|   | ||||
| @@ -16,8 +16,8 @@ import otroDestinoService from '../../services/Distribucion/otroDestinoService'; | ||||
| import type { SalidaOtroDestinoDto } from '../../models/dtos/Distribucion/SalidaOtroDestinoDto'; | ||||
| import type { CreateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/CreateSalidaOtroDestinoDto'; | ||||
| import type { UpdateSalidaOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateSalidaOtroDestinoDto'; | ||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto'; | ||||
| import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto'; | ||||
| import type { OtroDestinoDropdownDto } from '../../models/dtos/Distribucion/OtroDestinoDropdownDto'; | ||||
|  | ||||
| import SalidaOtroDestinoFormModal from '../../components/Modals/Distribucion/SalidaOtroDestinoFormModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| @@ -34,8 +34,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => { | ||||
|   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); | ||||
|   const [filtroIdDestino, setFiltroIdDestino] = useState<number | string>(''); | ||||
|  | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); | ||||
|   const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDto[]>([]); | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]); | ||||
|   const [otrosDestinos, setOtrosDestinos] = useState<OtroDestinoDropdownDto[]>([]); | ||||
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
| @@ -68,8 +68,8 @@ const GestionarSalidasOtrosDestinosPage: React.FC = () => { | ||||
|     setLoadingFiltersDropdown(true); | ||||
|     try { | ||||
|       const [pubsData, destinosData] = await Promise.all([ | ||||
|         publicacionService.getAllPublicaciones(undefined, undefined, true), | ||||
|         otroDestinoService.getAllOtrosDestinos() | ||||
|         publicacionService.getPublicacionesForDropdown(true), | ||||
|         otroDestinoService.getAllDropdownOtrosDestinos() | ||||
|       ]); | ||||
|       setPublicaciones(pubsData); | ||||
|       setOtrosDestinos(destinosData); | ||||
|   | ||||
| @@ -36,7 +36,6 @@ const GestionarEstadosBobinaPage: React.FC = () => { | ||||
|  | ||||
|   const { tienePermiso, isSuperAdmin } = usePermissions(); | ||||
|  | ||||
|   // Permisos para Estados de Bobina (ej: IB010 a IB013) | ||||
|   const puedeVer = isSuperAdmin || tienePermiso("IB010"); | ||||
|   const puedeCrear = isSuperAdmin || tienePermiso("IB011"); | ||||
|   const puedeModificar = isSuperAdmin || tienePermiso("IB012"); | ||||
|   | ||||
| @@ -21,8 +21,8 @@ import type { CreateStockBobinaDto } from '../../models/dtos/Impresion/CreateSto | ||||
| import type { UpdateStockBobinaDto } from '../../models/dtos/Impresion/UpdateStockBobinaDto'; | ||||
| import type { CambiarEstadoBobinaDto } from '../../models/dtos/Impresion/CambiarEstadoBobinaDto'; | ||||
| import type { TipoBobinaDto } from '../../models/dtos/Impresion/TipoBobinaDto'; | ||||
| import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; | ||||
| import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; | ||||
| import type { PlantaDropdownDto } from '../../models/dtos/Impresion/PlantaDropdownDto'; | ||||
| import type { EstadoBobinaDropdownDto } from '../../models/dtos/Impresion/EstadoBobinaDropdownDto'; | ||||
|  | ||||
| import StockBobinaIngresoFormModal from '../../components/Modals/Impresion/StockBobinaIngresoFormModal'; | ||||
| import StockBobinaEditFormModal from '../../components/Modals/Impresion/StockBobinaEditFormModal'; | ||||
| @@ -50,8 +50,8 @@ const GestionarStockBobinasPage: React.FC = () => { | ||||
|   const [filtroFechaHasta, setFiltroFechaHasta] = useState<string>(new Date().toISOString().split('T')[0]); | ||||
|  | ||||
|   const [tiposBobina, setTiposBobina] = useState<TipoBobinaDto[]>([]); | ||||
|   const [plantas, setPlantas] = useState<PlantaDto[]>([]); | ||||
|   const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDto[]>([]); | ||||
|   const [plantas, setPlantas] = useState<PlantaDropdownDto[]>([]); | ||||
|   const [estadosBobina, setEstadosBobina] = useState<EstadoBobinaDropdownDto[]>([]); | ||||
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); | ||||
|  | ||||
|   const [ingresoModalOpen, setIngresoModalOpen] = useState(false); | ||||
| @@ -76,9 +76,9 @@ const GestionarStockBobinasPage: React.FC = () => { | ||||
|     setLoadingFiltersDropdown(true); | ||||
|     try { | ||||
|       const [tiposData, plantasData, estadosData] = await Promise.all([ | ||||
|         tipoBobinaService.getAllTiposBobina(), | ||||
|         plantaService.getAllPlantas(), | ||||
|         estadoBobinaService.getAllEstadosBobina() | ||||
|         tipoBobinaService.getAllDropdownTiposBobina(), | ||||
|         plantaService.getPlantasForDropdown(), | ||||
|         estadoBobinaService.getAllDropdownEstadosBobina() | ||||
|       ]); | ||||
|       setTiposBobina(tiposData); | ||||
|       setPlantas(plantasData); | ||||
|   | ||||
| @@ -18,8 +18,8 @@ import plantaService from '../../services/Impresion/plantaService'; // Para filt | ||||
|  | ||||
| import type { TiradaDto } from '../../models/dtos/Impresion/TiradaDto'; | ||||
| import type { CreateTiradaRequestDto } from '../../models/dtos/Impresion/CreateTiradaRequestDto'; | ||||
| import type { PublicacionDto } from '../../models/dtos/Distribucion/PublicacionDto'; | ||||
| import type { PlantaDto } from '../../models/dtos/Impresion/PlantaDto'; | ||||
| import type { PublicacionDropdownDto } from '../../models/dtos/Distribucion/PublicacionDropdownDto'; | ||||
| import type { PlantaDropdownDto } from '../../models/dtos/Impresion/PlantaDropdownDto'; | ||||
|  | ||||
| import TiradaFormModal from '../../components/Modals/Impresion/TiradaFormModal'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
| @@ -36,8 +36,8 @@ const GestionarTiradasPage: React.FC = () => { | ||||
|   const [filtroIdPublicacion, setFiltroIdPublicacion] = useState<number | string>(''); | ||||
|   const [filtroIdPlanta, setFiltroIdPlanta] = useState<number | string>(''); | ||||
|  | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDto[]>([]); | ||||
|   const [plantas, setPlantas] = useState<PlantaDto[]>([]); | ||||
|   const [publicaciones, setPublicaciones] = useState<PublicacionDropdownDto[]>([]); | ||||
|   const [plantas, setPlantas] = useState<PlantaDropdownDto[]>([]); | ||||
|   const [loadingFiltersDropdown, setLoadingFiltersDropdown] = useState(false); | ||||
|  | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
| @@ -52,8 +52,8 @@ const GestionarTiradasPage: React.FC = () => { | ||||
|     setLoadingFiltersDropdown(true); | ||||
|     try { | ||||
|         const [pubsData, plantasData] = await Promise.all([ | ||||
|             publicacionService.getAllPublicaciones(undefined, undefined, true), | ||||
|             plantaService.getAllPlantas() | ||||
|             publicacionService.getPublicacionesForDropdown(true), | ||||
|             plantaService.getPlantasForDropdown() | ||||
|         ]); | ||||
|         setPublicaciones(pubsData); | ||||
|         setPlantas(plantasData); | ||||
|   | ||||
| @@ -8,67 +8,68 @@ import SaveIcon from '@mui/icons-material/Save'; | ||||
| import perfilService from '../../services/Usuarios/perfilService'; | ||||
| import type { PermisoAsignadoDto } from '../../models/dtos/Usuarios/PermisoAsignadoDto'; | ||||
| import type { PerfilDto } from '../../models/dtos/Usuarios/PerfilDto'; | ||||
| import { usePermissions as usePagePermissions } from '../../hooks/usePermissions'; // Renombrar para evitar conflicto | ||||
| import { usePermissions as usePagePermissions } from '../../hooks/usePermissions'; | ||||
| import axios from 'axios'; | ||||
| import PermisosChecklist from '../../components/Modals/Usuarios/PermisosChecklist'; | ||||
|  | ||||
| const SECCION_PERMISSIONS_PREFIX = "SS"; | ||||
|  | ||||
| const getModuloFromSeccionCodAcc = (codAcc: string): string | null => { | ||||
|     if (codAcc === "SS001") return "Distribución"; | ||||
|     if (codAcc === "SS002") return "Contables"; | ||||
|     if (codAcc === "SS003") return "Impresión"; | ||||
|     if (codAcc === "SS004") return "Reportes"; | ||||
|     if (codAcc === "SS005") return "Radios"; | ||||
|     if (codAcc === "SS006") return "Usuarios"; | ||||
|     return null; | ||||
|   if (codAcc === "SS001") return "Distribución"; | ||||
|   if (codAcc === "SS002") return "Contables"; | ||||
|   if (codAcc === "SS003") return "Impresión"; | ||||
|   if (codAcc === "SS004") return "Reportes"; | ||||
|   if (codAcc === "SS005") return "Radios"; | ||||
|   if (codAcc === "SS006") return "Usuarios"; | ||||
|   return null; | ||||
| }; | ||||
|  | ||||
| const getModuloConceptualDelPermiso = (permisoModulo: string): string => { | ||||
|     const moduloLower = permisoModulo.toLowerCase(); | ||||
|     if (moduloLower.includes("distribuidores") || | ||||
|         moduloLower.includes("canillas") || | ||||
|         moduloLower.includes("publicaciones distribución") || | ||||
|         moduloLower.includes("zonas distribuidores") || | ||||
|         moduloLower.includes("movimientos distribuidores") || | ||||
|         moduloLower.includes("empresas") || | ||||
|         moduloLower.includes("otros destinos") || | ||||
|         moduloLower.includes("ctrl. devoluciones") || | ||||
|         moduloLower.includes("movimientos canillas") || | ||||
|         moduloLower.includes("salidas otros destinos")) { | ||||
|         return "Distribución"; | ||||
|     } | ||||
|     if (moduloLower.includes("cuentas pagos") || | ||||
|         moduloLower.includes("cuentas notas") || | ||||
|         moduloLower.includes("cuentas tipos pagos")) { | ||||
|         return "Contables"; | ||||
|     } | ||||
|     if (moduloLower.includes("impresión tiradas") || | ||||
|         moduloLower.includes("impresión bobinas") || | ||||
|         moduloLower.includes("impresión plantas") || | ||||
|         moduloLower.includes("tipos bobinas")) { | ||||
|         return "Impresión"; | ||||
|     } | ||||
|     if (moduloLower.includes("radios")) { | ||||
|         return "Radios"; | ||||
|     } | ||||
|     if (moduloLower.includes("usuarios") || | ||||
|         moduloLower.includes("perfiles")) { | ||||
|         return "Usuarios"; | ||||
|     } | ||||
|     if (moduloLower.includes("reportes")) { | ||||
|         return "Reportes"; | ||||
|     } | ||||
|      if (moduloLower.includes("permisos")) { | ||||
|       return "Permisos (Definición)"; | ||||
|     } | ||||
|     return permisoModulo; | ||||
|   const moduloLower = permisoModulo.toLowerCase(); | ||||
|   if (moduloLower.includes("distribuidores") || | ||||
|     moduloLower.includes("canillas") || | ||||
|     moduloLower.includes("publicaciones distribución") || | ||||
|     moduloLower.includes("zonas distribuidores") || | ||||
|     moduloLower.includes("movimientos distribuidores") || | ||||
|     moduloLower.includes("empresas") || | ||||
|     moduloLower.includes("otros destinos") || | ||||
|     moduloLower.includes("ctrl. devoluciones") || | ||||
|     moduloLower.includes("movimientos canillas") || | ||||
|     moduloLower.includes("salidas otros destinos")) { | ||||
|     return "Distribución"; | ||||
|   } | ||||
|   if (moduloLower.includes("cuentas pagos") || | ||||
|     moduloLower.includes("cuentas notas") || | ||||
|     moduloLower.includes("cuentas tipos pagos")) { | ||||
|     return "Contables"; | ||||
|   } | ||||
|   if (moduloLower.includes("impresión tiradas") || | ||||
|     moduloLower.includes("impresión bobinas") || | ||||
|     moduloLower.includes("impresión plantas") || | ||||
|     moduloLower.includes("estados bobinas") || | ||||
|     moduloLower.includes("tipos bobinas")) { | ||||
|     return "Impresión"; | ||||
|   } | ||||
|   if (moduloLower.includes("radios")) { | ||||
|     return "Radios"; | ||||
|   } | ||||
|   if (moduloLower.includes("usuarios") || | ||||
|     moduloLower.includes("perfiles")) { | ||||
|     return "Usuarios"; | ||||
|   } | ||||
|   if (moduloLower.includes("reportes")) { | ||||
|     return "Reportes"; | ||||
|   } | ||||
|   if (moduloLower.includes("permisos")) { | ||||
|     return "Permisos (Definición)"; | ||||
|   } | ||||
|   return permisoModulo; | ||||
| }; | ||||
|  | ||||
| const AsignarPermisosAPerfilPage: React.FC = () => { | ||||
|   const { idPerfil } = useParams<{ idPerfil: string }>(); | ||||
|   const navigate = useNavigate(); | ||||
|   const { tienePermiso: tienePermisoPagina, isSuperAdmin } = usePagePermissions(); // Renombrado | ||||
|   const { tienePermiso: tienePermisoPagina, isSuperAdmin } = usePagePermissions(); | ||||
|  | ||||
|   const puedeAsignar = isSuperAdmin || tienePermisoPagina("PU004"); | ||||
|  | ||||
| @@ -124,76 +125,75 @@ const AsignarPermisosAPerfilPage: React.FC = () => { | ||||
|     moduloConceptualAsociado?: string // Este es el módulo conceptual del padre SSxxx o del grupo del hijo | ||||
|   ) => { | ||||
|     setPermisosSeleccionados(prevSelected => { | ||||
|         const newSelected = new Set(prevSelected); | ||||
|         const permisoActual = permisosDisponibles.find(p => p.id === permisoId); | ||||
|         if (!permisoActual) return prevSelected; | ||||
|       const newSelected = new Set(prevSelected); | ||||
|       const permisoActual = permisosDisponibles.find(p => p.id === permisoId); | ||||
|       if (!permisoActual) return prevSelected; | ||||
|  | ||||
|         const permisosDelModuloHijo = moduloConceptualAsociado | ||||
|             ? permisosDisponibles.filter(p => { | ||||
|                 const mc = getModuloConceptualDelPermiso(p.modulo); // Usar la función helper | ||||
|                 return mc === moduloConceptualAsociado && !p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX); | ||||
|             }) | ||||
|             : []; | ||||
|       const permisosDelModuloHijo = moduloConceptualAsociado | ||||
|         ? permisosDisponibles.filter(p => { | ||||
|           const mc = getModuloConceptualDelPermiso(p.modulo); // Usar la función helper | ||||
|           return mc === moduloConceptualAsociado && !p.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX); | ||||
|         }) | ||||
|         : []; | ||||
|  | ||||
|         if (esPermisoSeccionClick && moduloConceptualAsociado) { | ||||
|             const idPermisoSeccion = permisoActual.id; | ||||
|             const estabaSeccionSeleccionada = prevSelected.has(idPermisoSeccion); | ||||
|             const todosHijosEstabanSeleccionados = permisosDelModuloHijo.length > 0 && permisosDelModuloHijo.every(p => prevSelected.has(p.id)); | ||||
|             const ningunHijoEstabaSeleccionado = permisosDelModuloHijo.every(p => !prevSelected.has(p.id)); | ||||
|       if (esPermisoSeccionClick && moduloConceptualAsociado) { | ||||
|         const idPermisoSeccion = permisoActual.id; | ||||
|         const estabaSeccionSeleccionada = prevSelected.has(idPermisoSeccion); | ||||
|         const todosHijosEstabanSeleccionados = permisosDelModuloHijo.length > 0 && permisosDelModuloHijo.every(p => prevSelected.has(p.id)); | ||||
|         const ningunHijoEstabaSeleccionado = permisosDelModuloHijo.every(p => !prevSelected.has(p.id)); | ||||
|  | ||||
|  | ||||
|             if (!estabaSeccionSeleccionada) { // Estaba Off, pasa a "Solo Sección" (Indeterminate si hay hijos) | ||||
|                 newSelected.add(idPermisoSeccion); | ||||
|                 // NO se marcan los hijos | ||||
|             } else if (estabaSeccionSeleccionada && (ningunHijoEstabaSeleccionado || !todosHijosEstabanSeleccionados) && permisosDelModuloHijo.length > 0 ) { | ||||
|                 // Estaba "Solo Sección" o "Parcial Hijos", pasa a "Sección + Todos los Hijos" | ||||
|                 newSelected.add(idPermisoSeccion); // Asegurar | ||||
|                 permisosDelModuloHijo.forEach(p => newSelected.add(p.id)); | ||||
|             } else { // Estaba "Sección + Todos los Hijos" (o no había hijos), pasa a Off | ||||
|                 newSelected.delete(idPermisoSeccion); | ||||
|                 permisosDelModuloHijo.forEach(p => newSelected.delete(p.id)); | ||||
|             } | ||||
|  | ||||
|         } else if (!esPermisoSeccionClick && moduloConceptualAsociado) { // Clic en un permiso hijo | ||||
|             if (asignadoViaCheckboxHijo) { | ||||
|                 newSelected.add(permisoId); | ||||
|                 const permisoSeccionPadre = permisosDisponibles.find( | ||||
|                     ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado | ||||
|                 ); | ||||
|                 if (permisoSeccionPadre && !newSelected.has(permisoSeccionPadre.id)) { | ||||
|                     newSelected.add(permisoSeccionPadre.id); // Marcar padre si no estaba | ||||
|                 } | ||||
|             } else { // Desmarcando un hijo | ||||
|                 newSelected.delete(permisoId); | ||||
|                 const permisoSeccionPadre = permisosDisponibles.find( | ||||
|                     ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado | ||||
|                 ); | ||||
|                 if (permisoSeccionPadre) { | ||||
|                     const algunOtroHijoSeleccionado = permisosDelModuloHijo.some(p => p.id !== permisoId && newSelected.has(p.id)); | ||||
|                     if (!algunOtroHijoSeleccionado && newSelected.has(permisoSeccionPadre.id)) { | ||||
|                          // Si era el último hijo y el padre estaba marcado, NO desmarcamos el padre automáticamente. | ||||
|                          // El estado indeterminate se encargará visualmente. | ||||
|                          // Si quisiéramos que se desmarque el padre, aquí iría: newSelected.delete(permisoSeccionPadre.id); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } else { // Permiso sin módulo conceptual asociado (ej: "Permisos (Definición)") | ||||
|              if (asignadoViaCheckboxHijo) { | ||||
|                 newSelected.add(permisoId); | ||||
|             } else { | ||||
|                 newSelected.delete(permisoId); | ||||
|             } | ||||
|         if (!estabaSeccionSeleccionada) { // Estaba Off, pasa a "Solo Sección" (Indeterminate si hay hijos) | ||||
|           newSelected.add(idPermisoSeccion); | ||||
|           // NO se marcan los hijos | ||||
|         } else if (estabaSeccionSeleccionada && (ningunHijoEstabaSeleccionado || !todosHijosEstabanSeleccionados) && permisosDelModuloHijo.length > 0) { | ||||
|           // Estaba "Solo Sección" o "Parcial Hijos", pasa a "Sección + Todos los Hijos" | ||||
|           newSelected.add(idPermisoSeccion); // Asegurar | ||||
|           permisosDelModuloHijo.forEach(p => newSelected.add(p.id)); | ||||
|         } else { // Estaba "Sección + Todos los Hijos" (o no había hijos), pasa a Off | ||||
|           newSelected.delete(idPermisoSeccion); | ||||
|           permisosDelModuloHijo.forEach(p => newSelected.delete(p.id)); | ||||
|         } | ||||
|  | ||||
|         if (successMessage) setSuccessMessage(null); | ||||
|         if (error) setError(null); | ||||
|         return newSelected; | ||||
|       } else if (!esPermisoSeccionClick && moduloConceptualAsociado) { // Clic en un permiso hijo | ||||
|         if (asignadoViaCheckboxHijo) { | ||||
|           newSelected.add(permisoId); | ||||
|           const permisoSeccionPadre = permisosDisponibles.find( | ||||
|             ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado | ||||
|           ); | ||||
|           if (permisoSeccionPadre && !newSelected.has(permisoSeccionPadre.id)) { | ||||
|             newSelected.add(permisoSeccionPadre.id); // Marcar padre si no estaba | ||||
|           } | ||||
|         } else { // Desmarcando un hijo | ||||
|           newSelected.delete(permisoId); | ||||
|           const permisoSeccionPadre = permisosDisponibles.find( | ||||
|             ps => ps.codAcc.startsWith(SECCION_PERMISSIONS_PREFIX) && getModuloFromSeccionCodAcc(ps.codAcc) === moduloConceptualAsociado | ||||
|           ); | ||||
|           if (permisoSeccionPadre) { | ||||
|             const algunOtroHijoSeleccionado = permisosDelModuloHijo.some(p => p.id !== permisoId && newSelected.has(p.id)); | ||||
|             if (!algunOtroHijoSeleccionado && newSelected.has(permisoSeccionPadre.id)) { | ||||
|               // Si era el último hijo y el padre estaba marcado, NO desmarcamos el padre automáticamente. | ||||
|               // El estado indeterminate se encargará visualmente. | ||||
|               // Si quisiéramos que se desmarque el padre, aquí iría: newSelected.delete(permisoSeccionPadre.id); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } else { // Permiso sin módulo conceptual asociado (ej: "Permisos (Definición)") | ||||
|         if (asignadoViaCheckboxHijo) { | ||||
|           newSelected.add(permisoId); | ||||
|         } else { | ||||
|           newSelected.delete(permisoId); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (successMessage) setSuccessMessage(null); | ||||
|       if (error) setError(null); | ||||
|       return newSelected; | ||||
|     }); | ||||
|   }, [permisosDisponibles, successMessage, error]); | ||||
|  | ||||
|  | ||||
|   const handleGuardarCambios = async () => { | ||||
|     // ... (sin cambios) ... | ||||
|     if (!puedeAsignar || !perfil) return; | ||||
|     setSaving(true); setError(null); setSuccessMessage(null); | ||||
|     try { | ||||
| @@ -214,54 +214,54 @@ const AsignarPermisosAPerfilPage: React.FC = () => { | ||||
|   }; | ||||
|  | ||||
|   if (loading) { | ||||
|         return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>; | ||||
|     } | ||||
|     return <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}><CircularProgress /></Box>; | ||||
|   } | ||||
|  | ||||
|     if (error && !perfil) { | ||||
|         return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>; | ||||
|     } | ||||
|     if (!puedeAsignar) { | ||||
|         return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>; | ||||
|     } | ||||
|     if (!perfil && !loading) { | ||||
|         return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado o error al cargar.</Alert>; | ||||
|     } | ||||
|   if (error && !perfil) { | ||||
|     return <Alert severity="error" sx={{ m: 2 }}>{error}</Alert>; | ||||
|   } | ||||
|   if (!puedeAsignar) { | ||||
|     return <Alert severity="error" sx={{ m: 2 }}>Acceso denegado.</Alert>; | ||||
|   } | ||||
|   if (!perfil && !loading) { | ||||
|     return <Alert severity="warning" sx={{ m: 2 }}>Perfil no encontrado o error al cargar.</Alert>; | ||||
|   } | ||||
|  | ||||
|     return ( | ||||
|         <Box sx={{ p: 1 }}> | ||||
|             <Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}> | ||||
|                 Volver a Perfiles | ||||
|             </Button> | ||||
|             <Typography variant="h5" gutterBottom> | ||||
|                 Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'} | ||||
|             </Typography> | ||||
|             <Typography variant="body2" color="textSecondary" gutterBottom> | ||||
|                 ID Perfil: {perfil?.id} | ||||
|             </Typography> | ||||
|   return ( | ||||
|     <Box sx={{ p: 1 }}> | ||||
|       <Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/usuarios/perfiles')} sx={{ mb: 2 }}> | ||||
|         Volver a Perfiles | ||||
|       </Button> | ||||
|       <Typography variant="h5" gutterBottom> | ||||
|         Asignar Permisos al Perfil: {perfil?.nombrePerfil || 'Cargando...'} | ||||
|       </Typography> | ||||
|       <Typography variant="body2" color="textSecondary" gutterBottom> | ||||
|         ID Perfil: {perfil?.id} | ||||
|       </Typography> | ||||
|  | ||||
|             {error && !successMessage && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>} | ||||
|             {successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>} | ||||
|       {error && !successMessage && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>} | ||||
|       {successMessage && <Alert severity="success" sx={{ mb: 2 }}>{successMessage}</Alert>} | ||||
|  | ||||
|             <Paper sx={{ p: { xs: 1, sm: 2 }, mt: 2 }}> | ||||
|                 <PermisosChecklist | ||||
|                     permisosDisponibles={permisosDisponibles} | ||||
|                     permisosSeleccionados={permisosSeleccionados} | ||||
|                     onPermisoChange={handlePermisoChange} | ||||
|                     disabled={saving} | ||||
|                 /> | ||||
|                 <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}> | ||||
|                     <Button | ||||
|                         variant="contained" | ||||
|                         color="primary" | ||||
|                         startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />} | ||||
|                         onClick={handleGuardarCambios} | ||||
|                         disabled={saving || !puedeAsignar} | ||||
|                     > | ||||
|                         Guardar Cambios | ||||
|                     </Button> | ||||
|                 </Box> | ||||
|             </Paper> | ||||
|       <Paper sx={{ p: { xs: 1, sm: 2 }, mt: 2 }}> | ||||
|         <PermisosChecklist | ||||
|           permisosDisponibles={permisosDisponibles} | ||||
|           permisosSeleccionados={permisosSeleccionados} | ||||
|           onPermisoChange={handlePermisoChange} | ||||
|           disabled={saving} | ||||
|         /> | ||||
|         <Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}> | ||||
|           <Button | ||||
|             variant="contained" | ||||
|             color="primary" | ||||
|             startIcon={saving ? <CircularProgress size={20} color="inherit" /> : <SaveIcon />} | ||||
|             onClick={handleGuardarCambios} | ||||
|             disabled={saving || !puedeAsignar} | ||||
|           > | ||||
|             Guardar Cambios | ||||
|           </Button> | ||||
|         </Box> | ||||
|     ); | ||||
|       </Paper> | ||||
|     </Box> | ||||
|   ); | ||||
| }; | ||||
| export default AsignarPermisosAPerfilPage; | ||||
| @@ -1,6 +1,7 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import React, { useState, useEffect, useMemo } from 'react'; | ||||
| import { Box, Tabs, Tab, Paper, Typography } from '@mui/material'; | ||||
| import { Outlet, useNavigate, useLocation } from 'react-router-dom'; | ||||
| import { usePermissions } from '../../hooks/usePermissions'; | ||||
|  | ||||
| const usuariosSubModules = [ | ||||
|   { label: 'Perfiles', path: 'perfiles' }, | ||||
| @@ -13,37 +14,54 @@ const UsuariosIndexPage: React.FC = () => { | ||||
|   const navigate = useNavigate(); | ||||
|   const location = useLocation(); | ||||
|   const [selectedSubTab, setSelectedSubTab] = useState<number | false>(false); | ||||
|   const { isSuperAdmin } = usePermissions(); | ||||
|  | ||||
|   // --- Filtrar solo lo que puede ver este usuario --- | ||||
|   const availableSubModules = useMemo( | ||||
|     () => | ||||
|       usuariosSubModules.filter(sub => { | ||||
|         // Estos dos ítems solo para superadmins | ||||
|         if ( | ||||
|           (sub.path === 'permisos' || sub.path === 'auditoria-usuarios') | ||||
|           && !isSuperAdmin | ||||
|         ) { | ||||
|           return false; | ||||
|         } | ||||
|         return true; | ||||
|       }), | ||||
|     [isSuperAdmin] | ||||
|   ); | ||||
|  | ||||
|   // --- Ajustar la pestaña activa según la ruta --- | ||||
|   useEffect(() => { | ||||
|     const currentBasePath = '/usuarios'; | ||||
|     const subPath = location.pathname.startsWith(currentBasePath + '/') | ||||
|                       ? location.pathname.substring(currentBasePath.length + 1).split('/')[0] // Tomar solo la primera parte de la subruta | ||||
|                       : (location.pathname === currentBasePath ? usuariosSubModules[0]?.path : undefined); | ||||
|  | ||||
|     const activeTabIndex = usuariosSubModules.findIndex( | ||||
|       (subModule) => subModule.path === subPath | ||||
|     ); | ||||
|  | ||||
|     if (activeTabIndex !== -1) { | ||||
|       setSelectedSubTab(activeTabIndex); | ||||
|     } else { | ||||
|        if (location.pathname === currentBasePath && usuariosSubModules.length > 0) { | ||||
|          navigate(usuariosSubModules[0].path, { replace: true }); | ||||
|          setSelectedSubTab(0); | ||||
|        } else { | ||||
|           setSelectedSubTab(false); | ||||
|        } | ||||
|     const base = '/usuarios'; | ||||
|     let subPath: string | undefined; | ||||
|     if (location.pathname.startsWith(base + '/')) { | ||||
|       subPath = location.pathname.slice(base.length + 1).split('/')[0]; | ||||
|     } else if (location.pathname === base) { | ||||
|       subPath = availableSubModules[0]?.path; | ||||
|     } | ||||
|   }, [location.pathname, navigate]); | ||||
|     const idx = availableSubModules.findIndex(m => m.path === subPath); | ||||
|     if (idx !== -1) { | ||||
|       setSelectedSubTab(idx); | ||||
|     } else if (location.pathname === base && availableSubModules.length) { | ||||
|       navigate(availableSubModules[0].path, { replace: true }); | ||||
|       setSelectedSubTab(0); | ||||
|     } else { | ||||
|       setSelectedSubTab(false); | ||||
|     } | ||||
|   }, [location.pathname, navigate, availableSubModules]); | ||||
|  | ||||
|   const handleSubTabChange = (_event: React.SyntheticEvent, newValue: number) => { | ||||
|   const handleSubTabChange = (_: any, newValue: number) => { | ||||
|     setSelectedSubTab(newValue); | ||||
|     navigate(usuariosSubModules[newValue].path); | ||||
|     navigate(availableSubModules[newValue].path); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Box> | ||||
|       <Typography variant="h5" gutterBottom>Módulo de Usuarios y Seguridad</Typography> | ||||
|       <Typography variant="h5" gutterBottom> | ||||
|         Módulo de Usuarios y Seguridad | ||||
|       </Typography> | ||||
|       <Paper square elevation={1}> | ||||
|         <Tabs | ||||
|           value={selectedSubTab} | ||||
| @@ -54,8 +72,8 @@ const UsuariosIndexPage: React.FC = () => { | ||||
|           scrollButtons="auto" | ||||
|           aria-label="sub-módulos de usuarios" | ||||
|         > | ||||
|           {usuariosSubModules.map((subModule) => ( | ||||
|             <Tab key={subModule.path} label={subModule.label} /> | ||||
|           {availableSubModules.map(sub => ( | ||||
|             <Tab key={sub.path} label={sub.label} /> | ||||
|           ))} | ||||
|         </Tabs> | ||||
|       </Paper> | ||||
| @@ -66,4 +84,4 @@ const UsuariosIndexPage: React.FC = () => { | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export default UsuariosIndexPage; | ||||
| export default UsuariosIndexPage; | ||||
|   | ||||
| @@ -76,6 +76,9 @@ import GestionarNovedadesCanillaPage from '../pages/Distribucion/GestionarNoveda | ||||
| import ReporteNovedadesCanillasPage from '../pages/Reportes/ReporteNovedadesCanillasPage'; | ||||
| import ReporteListadoDistMensualPage from '../pages/Reportes/ReporteListadoDistMensualPage'; | ||||
|  | ||||
| // Anonalías | ||||
| import AlertasPage from '../pages/Anomalia/AlertasPage'; | ||||
|  | ||||
| // Auditorias | ||||
| import GestionarAuditoriaUsuariosPage from '../pages/Usuarios/Auditoria/GestionarAuditoriaUsuariosPage'; | ||||
| import AuditoriaGeneralPage from '../pages/Auditoria/AuditoriaGeneralPage'; | ||||
| @@ -130,6 +133,19 @@ const AppRoutes = () => { | ||||
|           {/* Rutas hijas que se renderizarán en el Outlet de MainLayoutWrapper */} | ||||
|           <Route index element={<HomePage />} /> {/* Para la ruta exacta "/" */} | ||||
|  | ||||
|           {/* Módulo de Anomalías */} | ||||
|           <Route | ||||
|             path="anomalias" | ||||
|             element={ | ||||
|               <SectionProtectedRoute requiredPermission="AL001" sectionName="Anomalías"> | ||||
|                 <Outlet /> | ||||
|               </SectionProtectedRoute> | ||||
|             } | ||||
|           > | ||||
|             <Route index element={<Navigate to="alertas" replace />} /> | ||||
|             <Route path="alertas" element={<AlertasPage />} /> | ||||
|           </Route> | ||||
|  | ||||
|           {/* Módulo de Distribución (anidado) */} | ||||
|           <Route | ||||
|             path="distribucion" | ||||
|   | ||||
							
								
								
									
										49
									
								
								Frontend/src/services/Anomalia/alertaService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								Frontend/src/services/Anomalia/alertaService.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import apiClient from '../apiClient'; | ||||
|  | ||||
| // El contrato que define la estructura de una alerta genérica | ||||
| export interface AlertaGenericaDto { | ||||
|   idAlerta: number; | ||||
|   fechaDeteccion: string; | ||||
|   tipoAlerta: string; | ||||
|   entidad: string; | ||||
|   idEntidad: number; | ||||
|   mensaje: string; | ||||
|   fechaAnomalia: string; | ||||
|   leida: boolean; | ||||
|   cantidadEnviada?: number; | ||||
|   cantidadDevuelta?: number; | ||||
|   porcentajeDevolucion?: number; | ||||
| } | ||||
|  | ||||
| // DTO para el request de marcar un grupo como leído | ||||
| export interface MarcarGrupoLeidoRequestDto { | ||||
|   tipoAlerta: string; | ||||
|   idEntidad: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Obtiene todas las alertas no leídas del sistema. | ||||
|  */ | ||||
| export const getAlertas = async (): Promise<AlertaGenericaDto[]> => { | ||||
|   try { | ||||
|     const response = await apiClient.get<AlertaGenericaDto[]>('/alertas'); | ||||
|     return response.data || []; | ||||
|   } catch (error) { | ||||
|     console.error("Error en getAlertas:", error); | ||||
|     return []; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Marca una única alerta como leída. | ||||
|  */ | ||||
| export const marcarAlertaLeida = async (idAlerta: number): Promise<void> => { | ||||
|   await apiClient.post(`/alertas/${idAlerta}/marcar-leida`); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Marca un grupo completo de alertas como leídas. | ||||
|  */ | ||||
| export const marcarGrupoComoLeido = async (request: MarcarGrupoLeidoRequestDto): Promise<void> => { | ||||
|     await apiClient.post('/alertas/marcar-grupo-leido', request); | ||||
| }; | ||||
| @@ -3,6 +3,7 @@ import type { CanillaDto } from '../../models/dtos/Distribucion/CanillaDto'; | ||||
| import type { CreateCanillaDto } from '../../models/dtos/Distribucion/CreateCanillaDto'; | ||||
| import type { UpdateCanillaDto } from '../../models/dtos/Distribucion/UpdateCanillaDto'; | ||||
| import type { ToggleBajaCanillaDto } from '../../models/dtos/Distribucion/ToggleBajaCanillaDto'; | ||||
| import type { CanillaDropdownDto } from '../../models/dtos/Distribucion/CanillaDropdownDto'; | ||||
|  | ||||
|  | ||||
| const getAllCanillas = async ( | ||||
| @@ -15,12 +16,24 @@ const getAllCanillas = async ( | ||||
|     if (nomApeFilter) params.nomApe = nomApeFilter; | ||||
|     if (legajoFilter !== undefined && legajoFilter !== null) params.legajo = legajoFilter; | ||||
|     if (soloActivos !== undefined) params.soloActivos = soloActivos; | ||||
|     if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter; // <<-- ¡CLAVE! Verifica esto. | ||||
|     if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter; | ||||
|  | ||||
|     const response = await apiClient.get<CanillaDto[]>('/canillas', { params }); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getAllDropdownCanillas = async ( | ||||
|     soloActivos?: boolean, | ||||
|     esAccionistaFilter?: boolean // Asegúrate que esté aquí | ||||
| ): Promise<CanillaDropdownDto[]> => { | ||||
|     const params: Record<string, string | number | boolean> = {}; | ||||
|     if (soloActivos !== undefined) params.soloActivos = soloActivos; | ||||
|     if (esAccionistaFilter !== undefined) params.esAccionista = esAccionistaFilter; | ||||
|  | ||||
|     const response = await apiClient.get<CanillaDropdownDto[]>('/canillas/dropdown', { params }); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getCanillaById = async (id: number): Promise<CanillaDto> => { | ||||
|     const response = await apiClient.get<CanillaDto>(`/canillas/${id}`); | ||||
|     return response.data; | ||||
| @@ -43,6 +56,7 @@ const toggleBajaCanilla = async (id: number, data: ToggleBajaCanillaDto): Promis | ||||
|  | ||||
| const canillaService = { | ||||
|     getAllCanillas, | ||||
|     getAllDropdownCanillas, | ||||
|     getCanillaById, | ||||
|     createCanilla, | ||||
|     updateCanilla, | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import apiClient from '../apiClient'; | ||||
| import type { OtroDestinoDto } from '../../models/dtos/Distribucion/OtroDestinoDto'; | ||||
| import type { CreateOtroDestinoDto } from '../../models/dtos/Distribucion/CreateOtroDestinoDto'; | ||||
| import type { UpdateOtroDestinoDto } from '../../models/dtos/Distribucion/UpdateOtroDestinoDto'; | ||||
| import type { OtroDestinoDropdownDto } from '../../models/dtos/Distribucion/OtroDestinoDropdownDto'; | ||||
|  | ||||
| const getAllOtrosDestinos = async (nombreFilter?: string): Promise<OtroDestinoDto[]> => { | ||||
|     const params: Record<string, string> = {}; | ||||
| @@ -12,6 +13,12 @@ const getAllOtrosDestinos = async (nombreFilter?: string): Promise<OtroDestinoDt | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getAllDropdownOtrosDestinos = async (): Promise<OtroDestinoDto[]> => { | ||||
|     // Llama a GET /api/otrosdestinos/dropdown | ||||
|     const response = await apiClient.get<OtroDestinoDropdownDto[]>('/otrosdestinos/dropdown'); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getOtroDestinoById = async (id: number): Promise<OtroDestinoDto> => { | ||||
|      // Llama a GET /api/otrosdestinos/{id} | ||||
|     const response = await apiClient.get<OtroDestinoDto>(`/otrosdestinos/${id}`); | ||||
| @@ -36,6 +43,7 @@ const deleteOtroDestino = async (id: number): Promise<void> => { | ||||
|  | ||||
| const otroDestinoService = { | ||||
|     getAllOtrosDestinos, | ||||
|     getAllDropdownOtrosDestinos, | ||||
|     getOtroDestinoById, | ||||
|     createOtroDestino, | ||||
|     updateOtroDestino, | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import apiClient from '../apiClient'; | ||||
| import type { EstadoBobinaDto } from '../../models/dtos/Impresion/EstadoBobinaDto'; | ||||
| import type { CreateEstadoBobinaDto } from '../../models/dtos/Impresion/CreateEstadoBobinaDto'; | ||||
| import type { UpdateEstadoBobinaDto } from '../../models/dtos/Impresion/UpdateEstadoBobinaDto'; | ||||
| import type { EstadoBobinaDropdownDto } from '../../models/dtos/Impresion/EstadoBobinaDropdownDto'; | ||||
|  | ||||
| const getAllEstadosBobina = async (denominacionFilter?: string): Promise<EstadoBobinaDto[]> => { | ||||
|     const params: Record<string, string> = {}; | ||||
| @@ -11,6 +12,11 @@ const getAllEstadosBobina = async (denominacionFilter?: string): Promise<EstadoB | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getAllDropdownEstadosBobina = async (): Promise<EstadoBobinaDropdownDto[]> => { | ||||
|     const response = await apiClient.get<EstadoBobinaDropdownDto[]>('/estadosbobina/dropdown'); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getEstadoBobinaById = async (id: number): Promise<EstadoBobinaDto> => { | ||||
|     const response = await apiClient.get<EstadoBobinaDto>(`/estadosbobina/${id}`); | ||||
|     return response.data; | ||||
| @@ -31,6 +37,7 @@ const deleteEstadoBobina = async (id: number): Promise<void> => { | ||||
|  | ||||
| const estadoBobinaService = { | ||||
|     getAllEstadosBobina, | ||||
|     getAllDropdownEstadosBobina, | ||||
|     getEstadoBobinaById, | ||||
|     createEstadoBobina, | ||||
|     updateEstadoBobina, | ||||
|   | ||||
| @@ -12,6 +12,12 @@ const getAllTiposBobina = async (denominacionFilter?: string): Promise<TipoBobin | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getAllDropdownTiposBobina = async (): Promise<TipoBobinaDto[]> => { | ||||
|     // Llama a GET /api/tiposbobina/dropdown | ||||
|     const response = await apiClient.get<TipoBobinaDto[]>('/tiposbobina/dropdown'); | ||||
|     return response.data; | ||||
| }; | ||||
|  | ||||
| const getTipoBobinaById = async (id: number): Promise<TipoBobinaDto> => { | ||||
|      // Llama a GET /api/tiposbobina/{id} | ||||
|     const response = await apiClient.get<TipoBobinaDto>(`/tiposbobina/${id}`); | ||||
| @@ -36,6 +42,7 @@ const deleteTipoBobina = async (id: number): Promise<void> => { | ||||
|  | ||||
| const tipoBobinaService = { | ||||
|     getAllTiposBobina, | ||||
|     getAllDropdownTiposBobina, | ||||
|     getTipoBobinaById, | ||||
|     createTipoBobina, | ||||
|     updateTipoBobina, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user